Merge branch 'main' into locales/hi
This commit is contained in:
commit
f295977bf0
5
.gitignore
vendored
5
.gitignore
vendored
@ -167,3 +167,8 @@ tsconfig.tsbuildinfo
|
||||
/apps/admin-x-settings/test-results/
|
||||
/apps/admin-x-settings/playwright-report/
|
||||
/apps/admin-x-settings/playwright/.cache/
|
||||
|
||||
# Tinybird
|
||||
.tinyb
|
||||
.venv
|
||||
.diff_tmp
|
||||
|
@ -59,14 +59,6 @@ const features = [{
|
||||
title: 'Content Visibility',
|
||||
description: 'Enables content visibility in Emails',
|
||||
flag: 'contentVisibility'
|
||||
},{
|
||||
title: 'Publish Flow — End Screen',
|
||||
description: 'Enables improved publish flow',
|
||||
flag: 'publishFlowEndScreen'
|
||||
},{
|
||||
title: 'Post Analytics — Refresh',
|
||||
description: 'Adds a refresh button to the post analytics screen',
|
||||
flag: 'postAnalyticsRefresh'
|
||||
}];
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="flex flex-column h-100 items-center overflow-auto" data-test-modal="publish-flow">
|
||||
<header class="gh-publish-header">
|
||||
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}} data-test-button="close-publish-flow">
|
||||
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
|
||||
<span>{{svg-jar "arrow-left"}} Editor</span>
|
||||
</button>
|
||||
|
||||
@ -45,14 +45,6 @@
|
||||
@close={{@close}}
|
||||
/>
|
||||
{{else if this.isComplete}}
|
||||
{{#unless (feature "publishFlowEndScreen")}}
|
||||
<Editor::Modals::PublishFlow::Complete
|
||||
@publishOptions={{@data.publishOptions}}
|
||||
@recipientType={{this.recipientType}}
|
||||
@postCount={{this.postCount}}
|
||||
@close={{@close}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
<Editor::Modals::PublishFlow::Options
|
||||
@publishOptions={{@data.publishOptions}}
|
||||
|
@ -93,7 +93,7 @@ export default class PublishFlowOptions extends Component {
|
||||
|
||||
try {
|
||||
yield this.args.saveTask.perform();
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
|
||||
if (this.args.publishOptions.isScheduled) {
|
||||
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
|
||||
id: this.args.publishOptions.post.id,
|
||||
@ -119,7 +119,6 @@ export default class PublishFlowOptions extends Component {
|
||||
this.router.transitionTo('pages');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === undefined && this.args.publishOptions.post.errors.length !== 0) {
|
||||
// validation error
|
||||
|
@ -103,10 +103,10 @@
|
||||
</footer>
|
||||
{{/if}}
|
||||
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}} data-test-button="close-publish-flow">{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
{{#unless this.post.emailOnly}}
|
||||
<a href="{{this.post.url}}" target="_blank" rel="noopener noreferrer" title="View post: {{this.post.title}}">
|
||||
<a href="{{this.post.url}}" target="_blank" rel="noopener noreferrer" title="View post: {{this.post.title}}" data-test-complete-bookmark>
|
||||
<div class="gh-post-card">
|
||||
{{#if this.post.featureImage}}
|
||||
<figure class="modal-image">
|
||||
|
@ -32,7 +32,7 @@
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
<p class="gh-content-entry-status">
|
||||
<p class="gh-content-entry-status" data-test-editor-post-status>
|
||||
<span class="published">
|
||||
Published
|
||||
{{#if @post.hasEmail}}
|
||||
@ -87,7 +87,7 @@
|
||||
<span class="gh-content-entry-date">– Lexical</span>
|
||||
{{/if}} --}}
|
||||
</p>
|
||||
<p class="gh-content-entry-status">
|
||||
<p class="gh-content-entry-status" data-test-editor-post-status>
|
||||
{{#if @post.isScheduled}}
|
||||
<span class="scheduled">
|
||||
Scheduled
|
||||
|
@ -16,10 +16,8 @@ export default class PostsList extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
}
|
||||
|
||||
async checkPublishFlowModal() {
|
||||
if (localStorage.getItem('ghost-last-scheduled-post')) {
|
||||
|
@ -34,9 +34,7 @@
|
||||
{{moment-format publishedAt "HH:mm"}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{#if (feature "publishFlowEndScreen")}}
|
||||
<div style="display: flex; gap: 8px;">
|
||||
{{#if (feature "postAnalyticsRefresh")}}
|
||||
<GhTaskButton
|
||||
@buttonText="Refresh"
|
||||
@task={{this.fetchPostTask}}
|
||||
@ -45,7 +43,6 @@
|
||||
@successText="Refreshed"
|
||||
@class="gh-btn gh-btn-icon refresh"
|
||||
@successClass="gh-btn gh-btn-icon refresh" />
|
||||
{{/if}}
|
||||
{{#unless this.post.emailOnly}}
|
||||
<button type="button" class="gh-post-list-cta share" {{on "click" this.togglePublishFlowModal}}>
|
||||
{{svg-jar "share" title="Share post"}}<span>Share</span>
|
||||
@ -89,11 +86,6 @@
|
||||
</GhDropdown>
|
||||
</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
|
||||
{{svg-jar "pen" title=""}}<span>Edit post</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</GhCanvasHeader>
|
||||
|
@ -50,10 +50,8 @@ export default class Analytics extends Component {
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
}
|
||||
|
||||
openPublishFlowModal() {
|
||||
this.modals.open(PostSuccessModal, {
|
||||
@ -73,13 +71,9 @@ export default class Analytics extends Component {
|
||||
}
|
||||
|
||||
get post() {
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
return this._post ?? this.args.post;
|
||||
}
|
||||
|
||||
return this.args.post;
|
||||
}
|
||||
|
||||
set post(value) {
|
||||
this._post = value;
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ export default class TopPages extends Component {
|
||||
|
||||
/**
|
||||
* @typedef {Object} Params
|
||||
* @property {string} cid
|
||||
* @property {string} site_uuid
|
||||
* @property {string} [date_from]
|
||||
* @property {string} [date_to]
|
||||
* @property {number} [limit]
|
||||
* @property {number} [skip]
|
||||
*/
|
||||
const params = {
|
||||
cid: this.config.stats.id,
|
||||
site_uuid: this.config.stats.id,
|
||||
date_from: startDate.format('YYYY-MM-DD'),
|
||||
date_to: endDate.format('YYYY-MM-DD')
|
||||
};
|
||||
|
@ -78,8 +78,6 @@ export default class FeatureService extends Service {
|
||||
@feature('ActivityPub') ActivityPub;
|
||||
@feature('editorExcerpt') editorExcerpt;
|
||||
@feature('contentVisibility') contentVisibility;
|
||||
@feature('publishFlowEndScreen') publishFlowEndScreen;
|
||||
@feature('postAnalyticsRefresh') postAnalyticsRefresh;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost-admin",
|
||||
"version": "5.90.2",
|
||||
"version": "5.91.0",
|
||||
"description": "Ember.js admin client for Ghost",
|
||||
"author": "Ghost Foundation",
|
||||
"homepage": "http://ghost.org",
|
||||
|
@ -141,6 +141,21 @@ function getWebmentionDiscoveryLink() {
|
||||
}
|
||||
}
|
||||
|
||||
function getTinybirdTrackerScript(dataRoot) {
|
||||
const scriptUrl = config.get('tinybird:tracker:scriptUrl');
|
||||
const endpoint = config.get('tinybird:tracker:endpoint');
|
||||
const token = config.get('tinybird:tracker:token');
|
||||
|
||||
const tbParams = _.map({
|
||||
site_uuid: config.get('tinybird:tracker:id'),
|
||||
post_uuid: dataRoot.post?.uuid,
|
||||
member_uuid: dataRoot.member?.uuid,
|
||||
member_status: dataRoot.member?.status
|
||||
}, (value, key) => `tb_${key}="${value}"`).join(' ');
|
||||
|
||||
return `<script defer src="${scriptUrl}" data-host="${endpoint}" data-token="${token}" ${tbParams}></script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* **NOTE**
|
||||
* Express adds `_locals`, see https://github.com/expressjs/express/blob/4.15.4/lib/response.js#L962.
|
||||
@ -319,6 +334,10 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
||||
if (!_.isEmpty(tagCodeInjection)) {
|
||||
head.push(tagCodeInjection);
|
||||
}
|
||||
|
||||
if (config.get('tinybird') && config.get('tinybird:tracker') && config.get('tinybird:tracker:scriptUrl')) {
|
||||
head.push(getTinybirdTrackerScript(dataRoot));
|
||||
}
|
||||
}
|
||||
|
||||
debug('end');
|
||||
|
@ -39,11 +39,16 @@ class RecommendationServiceWrapper {
|
||||
incomingRecommendationService;
|
||||
|
||||
init() {
|
||||
const config = require('../../../shared/config');
|
||||
if (config.get('services:recommendations:enabled') === false) {
|
||||
logging.info('[Recommendations] Service is disabled via config');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = require('../../../shared/config');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const models = require('../../models');
|
||||
const sentry = require('../../../shared/sentry');
|
||||
|
@ -45,9 +45,7 @@ const ALPHA_FEATURES = [
|
||||
'importMemberTier',
|
||||
'lexicalIndicators',
|
||||
'adminXDemo',
|
||||
'contentVisibility',
|
||||
'publishFlowEndScreen',
|
||||
'postAnalyticsRefresh'
|
||||
'contentVisibility'
|
||||
];
|
||||
|
||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost",
|
||||
"version": "5.90.2",
|
||||
"version": "5.91.0",
|
||||
"description": "The professional publishing platform",
|
||||
"author": "Ghost Foundation",
|
||||
"homepage": "https://ghost.org",
|
||||
|
@ -29,8 +29,6 @@ Object {
|
||||
"members": true,
|
||||
"newEmailAddresses": true,
|
||||
"outboundLinkTagging": true,
|
||||
"postAnalyticsRefresh": true,
|
||||
"publishFlowEndScreen": true,
|
||||
"stripeAutomaticTax": true,
|
||||
"themeErrorsNotification": true,
|
||||
"tipsAndDonations": true,
|
||||
|
@ -11,11 +11,11 @@ const {createTier, createMember, createPostDraft, impersonateMember} = require('
|
||||
* @param {string} [hoverStatus] Optional different status when you hover the status
|
||||
*/
|
||||
const checkPostStatus = async (page, status, hoverStatus) => {
|
||||
await expect(page.locator('[data-test-editor-post-status]')).toContainText(status, {timeout: 5000});
|
||||
await expect(page.locator('[data-test-editor-post-status]').first()).toContainText(status, {timeout: 5000});
|
||||
|
||||
if (hoverStatus) {
|
||||
await page.locator('[data-test-editor-post-status]').hover();
|
||||
await expect(page.locator('[data-test-editor-post-status]')).toContainText(hoverStatus, {timeout: 5000});
|
||||
await page.locator('[data-test-editor-post-status]').first().hover();
|
||||
await expect(page.locator('[data-test-editor-post-status]').first()).toContainText(hoverStatus, {timeout: 5000});
|
||||
}
|
||||
};
|
||||
|
||||
@ -198,8 +198,6 @@ test.describe('Publishing', () => {
|
||||
await createPostDraft(sharedPage, postData);
|
||||
await publishPost(sharedPage, {type: 'publish+send'});
|
||||
await closePublishFlow(sharedPage);
|
||||
|
||||
await checkPostStatus(sharedPage, 'Published');
|
||||
await checkPostPublished(sharedPage, postData);
|
||||
});
|
||||
|
||||
@ -232,8 +230,6 @@ test.describe('Publishing', () => {
|
||||
await createPostDraft(sharedPage, postData);
|
||||
await publishPost(sharedPage, {type: 'send'});
|
||||
await closePublishFlow(sharedPage);
|
||||
await checkPostStatus(sharedPage, 'Sent to '); // can't test for 1 member for now, because depends on test ordering :( (sometimes 2 members are created)
|
||||
|
||||
await checkPostNotPublished(sharedPage, postData);
|
||||
});
|
||||
});
|
||||
@ -327,8 +323,9 @@ test.describe('Publishing', () => {
|
||||
await expect(publishedHeader).toContainText(date.toFormat('LLL d, yyyy'));
|
||||
|
||||
// add some extra text to the post
|
||||
await adminPage.locator('li[data-test-post-id]').first().click();
|
||||
await adminPage.locator('[data-kg="editor"]').first().click();
|
||||
await adminPage.waitForTimeout(200); //
|
||||
await adminPage.waitForTimeout(500);
|
||||
await adminPage.keyboard.type(' This is some updated text.');
|
||||
|
||||
// change some post settings
|
||||
@ -431,7 +428,7 @@ test.describe('Publishing', () => {
|
||||
// Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
|
||||
await publishPost(sharedPage, {type: 'send', time: '00:00'});
|
||||
await closePublishFlow(sharedPage);
|
||||
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent to');
|
||||
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent in a few seconds');
|
||||
const editorUrl = await sharedPage.url();
|
||||
|
||||
// Check not published yet
|
||||
@ -472,6 +469,7 @@ test.describe('Publishing', () => {
|
||||
await checkPostNotPublished(testsharedPage, postData);
|
||||
|
||||
// Now unschedule this post
|
||||
await sharedPage.locator('li[data-test-post-id]').first().click();
|
||||
await sharedPage.locator('[data-test-button="update-flow"]').first().click();
|
||||
await sharedPage.locator('[data-test-button="revert-to-draft"]').click();
|
||||
|
||||
@ -566,6 +564,7 @@ test.describe('Updating post access', () => {
|
||||
|
||||
// publish
|
||||
await publishPost(sharedPage);
|
||||
await closePublishFlow(sharedPage);
|
||||
const frontendPage = await openPublishedPostBookmark(sharedPage);
|
||||
|
||||
// non-member doesn't have access
|
||||
@ -607,7 +606,6 @@ test.describe('Updating post access', () => {
|
||||
await closePublishFlow(page);
|
||||
|
||||
// go to settings and change the timezone
|
||||
await page.locator('[data-test-link="posts"]').click();
|
||||
await page.locator('[data-test-nav="settings"]').click();
|
||||
await expect(page.getByTestId('timezone')).toContainText('UTC');
|
||||
|
||||
|
@ -8,73 +8,75 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a
|
||||
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
|
||||
<title>👍 New recommendation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
@media only screen and (max-width: 620px) {
|
||||
table.body h1 {
|
||||
font-size: 22px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
|
||||
table.body p,
|
||||
table.body ul,
|
||||
table.body ol,
|
||||
table.body td,
|
||||
table.body span,
|
||||
table.body a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
|
||||
table.body .wrapper,
|
||||
table.body .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
|
||||
table.body .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
|
||||
table.body .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
|
||||
table.body .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
|
||||
table.body .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
table[class=body] p[class=small],
|
||||
table[class=body] a[class=small] {
|
||||
font-size: 11px !important;
|
||||
|
||||
table.body p.large,
|
||||
table.body p.large a {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
table.body p.small,
|
||||
table.body a.small {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.new-mention-thumbnail {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
}
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
/* Reset styles for Gmail (it wraps email address in link with custom styles) */
|
||||
|
||||
.text-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
@ -83,6 +85,15 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.text-link-accent a {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@ -91,31 +102,8 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 34px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #EEF5F8;
|
||||
}
|
||||
a {
|
||||
color: #15212A;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid #DDE1E5;
|
||||
}
|
||||
.recommendation-card--outlook {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
border: 1px solid #F9F9FA;
|
||||
background: #F9F9FA;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=\\"background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;\\">
|
||||
|
||||
@ -138,7 +126,7 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a
|
||||
|
||||
<!--[if !mso !vml]-->
|
||||
<figure style=\\"margin:0 0 1.5em;padding:0;width:100%;background:#F4F5F6;border-radius:8px;\\">
|
||||
<a style=\\"display:flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;background:#F4F5F6;border-radius:8px;color:#15171A;text-decoration:none\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<a style=\\"display: flex; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif; background: #F4F5F6; border-radius: 8px; color: #15171A; text-decoration: none;\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<div style=\\"display:inline-block; width:100%; padding:20px\\">
|
||||
<div style=\\"color:#15171A;font-size:13px;font-weight:400\\">
|
||||
|
||||
@ -179,7 +167,7 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; border-radius: 8px; text-align: center;\\">
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations\\" target=\\"_blank\\" style=\\"border:solid 1px #FF1A75;border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:#FF1A75;border-color:#FF1A75;color:#ffffff\\">View recommendations</a>
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations\\" target=\\"_blank\\" style=\\"border: solid 1px #FF1A75; border-radius: 8px; box-sizing: border-box; display: inline-block; font-size: 15px; font-weight: normal; margin: 0; padding: 10px 20px; text-decoration: none; background-color: #FF1A75; border-color: #FF1A75; color: #ffffff;\\">View recommendations</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -264,73 +252,75 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
|
||||
<title>👍 New recommendation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
@media only screen and (max-width: 620px) {
|
||||
table.body h1 {
|
||||
font-size: 22px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
|
||||
table.body p,
|
||||
table.body ul,
|
||||
table.body ol,
|
||||
table.body td,
|
||||
table.body span,
|
||||
table.body a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
|
||||
table.body .wrapper,
|
||||
table.body .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
|
||||
table.body .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
|
||||
table.body .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
|
||||
table.body .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
|
||||
table.body .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
table[class=body] p[class=small],
|
||||
table[class=body] a[class=small] {
|
||||
font-size: 11px !important;
|
||||
|
||||
table.body p.large,
|
||||
table.body p.large a {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
table.body p.small,
|
||||
table.body a.small {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.new-mention-thumbnail {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
}
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
/* Reset styles for Gmail (it wraps email address in link with custom styles) */
|
||||
|
||||
.text-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
@ -339,6 +329,15 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.text-link-accent a {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@ -347,31 +346,8 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 34px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #EEF5F8;
|
||||
}
|
||||
a {
|
||||
color: #15212A;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid #DDE1E5;
|
||||
}
|
||||
.recommendation-card--outlook {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
border: 1px solid #F9F9FA;
|
||||
background: #F9F9FA;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=\\"background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;\\">
|
||||
|
||||
@ -395,7 +371,7 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
|
||||
<!--[if !mso !vml]-->
|
||||
<figure style=\\"margin:0 0 1.5em;padding:0;width:100%;background:#F4F5F6;border-radius:8px;\\">
|
||||
<a style=\\"display:flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;background:#F4F5F6;border-radius:8px;color:#15171A;text-decoration:none\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<a style=\\"display: flex; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif; background: #F4F5F6; border-radius: 8px; color: #15171A; text-decoration: none;\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<div style=\\"display:inline-block; width:100%; padding:20px\\">
|
||||
<div style=\\"color:#15171A;font-size:13px;font-weight:400\\">
|
||||
|
||||
@ -436,7 +412,7 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; border-radius: 8px; text-align: center;\\">
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations/add?url=https%3A%2F%2Fwww.otherghostsite.com%2F\\" target=\\"_blank\\" style=\\"border:solid 1px #FF1A75;border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:#FF1A75;border-color:#FF1A75;color:#ffffff\\">Recommend back</a>
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations/add?url=https%3A%2F%2Fwww.otherghostsite.com%2F\\" target=\\"_blank\\" style=\\"border: solid 1px #FF1A75; border-radius: 8px; box-sizing: border-box; display: inline-block; font-size: 15px; font-weight: normal; margin: 0; padding: 10px 20px; text-decoration: none; background-color: #FF1A75; border-color: #FF1A75; color: #ffffff;\\">Recommend back</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -499,73 +475,75 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
|
||||
<title>👍 New recommendation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
@media only screen and (max-width: 620px) {
|
||||
table.body h1 {
|
||||
font-size: 22px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
|
||||
table.body p,
|
||||
table.body ul,
|
||||
table.body ol,
|
||||
table.body td,
|
||||
table.body span,
|
||||
table.body a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
|
||||
table.body .wrapper,
|
||||
table.body .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
|
||||
table.body .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
|
||||
table.body .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
|
||||
table.body .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
|
||||
table.body .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
table[class=body] p[class=small],
|
||||
table[class=body] a[class=small] {
|
||||
font-size: 11px !important;
|
||||
|
||||
table.body p.large,
|
||||
table.body p.large a {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
table.body p.small,
|
||||
table.body a.small {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.new-mention-thumbnail {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
}
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
/* Reset styles for Gmail (it wraps email address in link with custom styles) */
|
||||
|
||||
.text-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
@ -574,6 +552,15 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.text-link-accent a {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@ -582,31 +569,8 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 34px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #EEF5F8;
|
||||
}
|
||||
a {
|
||||
color: #15212A;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid #DDE1E5;
|
||||
}
|
||||
.recommendation-card--outlook {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
border: 1px solid #F9F9FA;
|
||||
background: #F9F9FA;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=\\"background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;\\">
|
||||
|
||||
@ -630,7 +594,7 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
|
||||
<!--[if !mso !vml]-->
|
||||
<figure style=\\"margin:0 0 1.5em;padding:0;width:100%;background:#F4F5F6;border-radius:8px;\\">
|
||||
<a style=\\"display:flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;background:#F4F5F6;border-radius:8px;color:#15171A;text-decoration:none\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<a style=\\"display: flex; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif; background: #F4F5F6; border-radius: 8px; color: #15171A; text-decoration: none;\\" href=\\"https://www.otherghostsite.com/\\">
|
||||
<div style=\\"display:inline-block; width:100%; padding:20px\\">
|
||||
<div style=\\"color:#15171A;font-size:13px;font-weight:400\\">
|
||||
|
||||
@ -671,7 +635,7 @@ exports[`Incoming Recommendation Emails Sends an email if we receive a recommend
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; border-radius: 8px; text-align: center;\\">
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations/add?url=https%3A%2F%2Fwww.otherghostsite.com%2F\\" target=\\"_blank\\" style=\\"border:solid 1px #FF1A75;border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:#FF1A75;border-color:#FF1A75;color:#ffffff\\">Recommend back</a>
|
||||
<a href=\\"http://127.0.0.1:2369/ghost/#/settings/recommendations/add?url=https%3A%2F%2Fwww.otherghostsite.com%2F\\" target=\\"_blank\\" style=\\"border: solid 1px #FF1A75; border-radius: 8px; box-sizing: border-box; display: inline-block; font-size: 15px; font-weight: normal; margin: 0; padding: 10px 20px; text-decoration: none; background-color: #FF1A75; border-color: #FF1A75; color: #ffffff;\\">Recommend back</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -959,6 +959,248 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`{{ghost_head}} helper includes tinybird tracker script when config is set Sets tb_post_uuid on post page 1 1`] = `
|
||||
Object {
|
||||
"rendered": "<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
|
||||
<link rel=\\"amphtml\\" href=\\"http://127.0.0.1:2369/post/amp/\\">
|
||||
|
||||
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:type\\" content=\\"article\\">
|
||||
<meta property=\\"og:title\\" content=\\"Testing stats\\">
|
||||
<meta property=\\"og:description\\" content=\\"Creating stats for the site\\">
|
||||
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta property=\\"article:published_time\\" content=\\"1970-01-01T00:00:00.000Z\\">
|
||||
<meta property=\\"article:modified_time\\" content=\\"1970-01-01T00:00:00.000Z\\">
|
||||
<meta property=\\"article:author\\" content=\\"https://www.facebook.com/testuser\\">
|
||||
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
|
||||
<meta name=\\"twitter:title\\" content=\\"Testing stats\\">
|
||||
<meta name=\\"twitter:description\\" content=\\"Creating stats for the site\\">
|
||||
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta name=\\"twitter:label1\\" content=\\"Written by\\">
|
||||
<meta name=\\"twitter:data1\\" content=\\"Author name\\">
|
||||
<meta name=\\"twitter:creator\\" content=\\"@testuser\\">
|
||||
|
||||
<script type=\\"application/ld+json\\">
|
||||
{
|
||||
\\"@context\\": \\"https://schema.org\\",
|
||||
\\"@type\\": \\"Article\\",
|
||||
\\"publisher\\": {
|
||||
\\"@type\\": \\"Organization\\",
|
||||
\\"name\\": \\"Ghost\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"logo\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
|
||||
}
|
||||
},
|
||||
\\"author\\": {
|
||||
\\"@type\\": \\"Person\\",
|
||||
\\"name\\": \\"Author name\\",
|
||||
\\"image\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/content/images/test-author-image.png\\"
|
||||
},
|
||||
\\"url\\": \\"https://mysite.com/fakeauthor/\\",
|
||||
\\"sameAs\\": [
|
||||
\\"http://authorwebsite.com\\",
|
||||
\\"https://www.facebook.com/testuser\\",
|
||||
\\"https://twitter.com/testuser\\"
|
||||
]
|
||||
},
|
||||
\\"headline\\": \\"Testing stats\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/post/\\",
|
||||
\\"datePublished\\": \\"1970-01-01T00:00:00.000Z\\",
|
||||
\\"dateModified\\": \\"1970-01-01T00:00:00.000Z\\",
|
||||
\\"description\\": \\"Creating stats for the site\\",
|
||||
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/post/\\"
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name=\\"generator\\" content=\\"Ghost 0.3\\">
|
||||
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
|
||||
|
||||
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
|
||||
|
||||
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
|
||||
<script defer src=\\"https://unpkg.com/@tinybirdco/flock.js\\" data-host=\\"https://api.tinybird.co\\" data-token=\\"tinybird_token\\" tb_site_uuid=\\"tb_test_site_uuid\\" tb_post_uuid=\\"post_uuid\\" tb_member_uuid=\\"undefined\\" tb_member_status=\\"undefined\\"></script>",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`{{ghost_head}} helper includes tinybird tracker script when config is set sets both tb_member_x variables and tb_post_uuid on logged in post page 1 1`] = `
|
||||
Object {
|
||||
"rendered": "<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
|
||||
<link rel=\\"amphtml\\" href=\\"http://127.0.0.1:2369/post/amp/\\">
|
||||
|
||||
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:type\\" content=\\"article\\">
|
||||
<meta property=\\"og:title\\" content=\\"Testing stats\\">
|
||||
<meta property=\\"og:description\\" content=\\"Creating stats for the site\\">
|
||||
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta property=\\"article:published_time\\" content=\\"1970-01-01T00:00:00.000Z\\">
|
||||
<meta property=\\"article:modified_time\\" content=\\"1970-01-01T00:00:00.000Z\\">
|
||||
<meta property=\\"article:author\\" content=\\"https://www.facebook.com/testuser\\">
|
||||
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
|
||||
<meta name=\\"twitter:title\\" content=\\"Testing stats\\">
|
||||
<meta name=\\"twitter:description\\" content=\\"Creating stats for the site\\">
|
||||
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/post/\\">
|
||||
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta name=\\"twitter:label1\\" content=\\"Written by\\">
|
||||
<meta name=\\"twitter:data1\\" content=\\"Author name\\">
|
||||
<meta name=\\"twitter:creator\\" content=\\"@testuser\\">
|
||||
|
||||
<script type=\\"application/ld+json\\">
|
||||
{
|
||||
\\"@context\\": \\"https://schema.org\\",
|
||||
\\"@type\\": \\"Article\\",
|
||||
\\"publisher\\": {
|
||||
\\"@type\\": \\"Organization\\",
|
||||
\\"name\\": \\"Ghost\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"logo\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
|
||||
}
|
||||
},
|
||||
\\"author\\": {
|
||||
\\"@type\\": \\"Person\\",
|
||||
\\"name\\": \\"Author name\\",
|
||||
\\"image\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/content/images/test-author-image.png\\"
|
||||
},
|
||||
\\"url\\": \\"https://mysite.com/fakeauthor/\\",
|
||||
\\"sameAs\\": [
|
||||
\\"http://authorwebsite.com\\",
|
||||
\\"https://www.facebook.com/testuser\\",
|
||||
\\"https://twitter.com/testuser\\"
|
||||
]
|
||||
},
|
||||
\\"headline\\": \\"Testing stats\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/post/\\",
|
||||
\\"datePublished\\": \\"1970-01-01T00:00:00.000Z\\",
|
||||
\\"dateModified\\": \\"1970-01-01T00:00:00.000Z\\",
|
||||
\\"description\\": \\"Creating stats for the site\\",
|
||||
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/post/\\"
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name=\\"generator\\" content=\\"Ghost 4.3\\">
|
||||
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
|
||||
|
||||
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
|
||||
|
||||
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
|
||||
<script defer src=\\"https://unpkg.com/@tinybirdco/flock.js\\" data-host=\\"https://api.tinybird.co\\" data-token=\\"tinybird_token\\" tb_site_uuid=\\"tb_test_site_uuid\\" tb_post_uuid=\\"post_uuid\\" tb_member_uuid=\\"member_uuid\\" tb_member_status=\\"free\\"></script>",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`{{ghost_head}} helper includes tinybird tracker script when config is set sets tb_member_x variables on logged in home page 1 1`] = `
|
||||
Object {
|
||||
"rendered": "<meta name=\\"description\\" content=\\"site description\\">
|
||||
<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/\\">
|
||||
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
|
||||
|
||||
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:type\\" content=\\"website\\">
|
||||
<meta property=\\"og:title\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:description\\" content=\\"site description\\">
|
||||
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/\\">
|
||||
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
|
||||
<meta name=\\"twitter:title\\" content=\\"Ghost\\">
|
||||
<meta name=\\"twitter:description\\" content=\\"site description\\">
|
||||
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/\\">
|
||||
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
|
||||
<script type=\\"application/ld+json\\">
|
||||
{
|
||||
\\"@context\\": \\"https://schema.org\\",
|
||||
\\"@type\\": \\"WebSite\\",
|
||||
\\"publisher\\": {
|
||||
\\"@type\\": \\"Organization\\",
|
||||
\\"name\\": \\"Ghost\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"logo\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
|
||||
}
|
||||
},
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"image\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/content/images/site-cover.png\\"
|
||||
},
|
||||
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"description\\": \\"site description\\"
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name=\\"generator\\" content=\\"Ghost 4.3\\">
|
||||
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
|
||||
|
||||
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
|
||||
|
||||
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
|
||||
<script defer src=\\"https://unpkg.com/@tinybirdco/flock.js\\" data-host=\\"https://api.tinybird.co\\" data-token=\\"tinybird_token\\" tb_site_uuid=\\"tb_test_site_uuid\\" tb_post_uuid=\\"undefined\\" tb_member_uuid=\\"member_uuid\\" tb_member_status=\\"paid\\"></script>",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`{{ghost_head}} helper includes tinybird tracker script when config is set with all tb_variables set to undefined on logged out home page 1 1`] = `
|
||||
Object {
|
||||
"rendered": "<meta name=\\"description\\" content=\\"site description\\">
|
||||
<link rel=\\"canonical\\" href=\\"http://127.0.0.1:2369/\\">
|
||||
<meta name=\\"referrer\\" content=\\"no-referrer-when-downgrade\\">
|
||||
|
||||
<meta property=\\"og:site_name\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:type\\" content=\\"website\\">
|
||||
<meta property=\\"og:title\\" content=\\"Ghost\\">
|
||||
<meta property=\\"og:description\\" content=\\"site description\\">
|
||||
<meta property=\\"og:url\\" content=\\"http://127.0.0.1:2369/\\">
|
||||
<meta property=\\"og:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
<meta name=\\"twitter:card\\" content=\\"summary_large_image\\">
|
||||
<meta name=\\"twitter:title\\" content=\\"Ghost\\">
|
||||
<meta name=\\"twitter:description\\" content=\\"site description\\">
|
||||
<meta name=\\"twitter:url\\" content=\\"http://127.0.0.1:2369/\\">
|
||||
<meta name=\\"twitter:image\\" content=\\"http://127.0.0.1:2369/content/images/site-cover.png\\">
|
||||
|
||||
<script type=\\"application/ld+json\\">
|
||||
{
|
||||
\\"@context\\": \\"https://schema.org\\",
|
||||
\\"@type\\": \\"WebSite\\",
|
||||
\\"publisher\\": {
|
||||
\\"@type\\": \\"Organization\\",
|
||||
\\"name\\": \\"Ghost\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"logo\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/favicon.ico\\"
|
||||
}
|
||||
},
|
||||
\\"url\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"image\\": {
|
||||
\\"@type\\": \\"ImageObject\\",
|
||||
\\"url\\": \\"http://127.0.0.1:2369/content/images/site-cover.png\\"
|
||||
},
|
||||
\\"mainEntityOfPage\\": \\"http://127.0.0.1:2369/\\",
|
||||
\\"description\\": \\"site description\\"
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name=\\"generator\\" content=\\"Ghost 4.3\\">
|
||||
<link rel=\\"alternate\\" type=\\"application/rss+xml\\" title=\\"Ghost\\" href=\\"http://localhost:65530/rss/\\">
|
||||
|
||||
<script defer src=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/ghost/sodo-search@~[[VERSION]]/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>
|
||||
|
||||
<link href=\\"http://127.0.0.1:2369/webmentions/receive/\\" rel=\\"webmention\\">
|
||||
<script defer src=\\"https://unpkg.com/@tinybirdco/flock.js\\" data-host=\\"https://api.tinybird.co\\" data-token=\\"tinybird_token\\" tb_site_uuid=\\"tb_test_site_uuid\\" tb_post_uuid=\\"undefined\\" tb_member_uuid=\\"undefined\\" tb_member_status=\\"undefined\\"></script>",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`{{ghost_head}} helper members scripts includes portal when members enabled 1 1`] = `
|
||||
Object {
|
||||
"rendered": "<meta name=\\"description\\" content=\\"site description\\">
|
||||
|
@ -340,6 +340,19 @@ describe('{{ghost_head}} helper', function () {
|
||||
published_at: new Date(0),
|
||||
updated_at: new Date(0)
|
||||
}));
|
||||
|
||||
posts.push(createPost({ // Post 10
|
||||
title: 'Testing stats',
|
||||
uuid: 'post_uuid',
|
||||
excerpt: 'Creating stats for the site',
|
||||
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('Creating stats for the site'),
|
||||
authors: [
|
||||
authors[3]
|
||||
],
|
||||
primary_author: authors[3],
|
||||
published_at: new Date(0),
|
||||
updated_at: new Date(0)
|
||||
}));
|
||||
};
|
||||
|
||||
before(function () {
|
||||
@ -1185,4 +1198,80 @@ describe('{{ghost_head}} helper', function () {
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('includes tinybird tracker script when config is set', function () {
|
||||
beforeEach(function () {
|
||||
configUtils.set({
|
||||
tinybird: {
|
||||
tracker: {
|
||||
scriptUrl: 'https://unpkg.com/@tinybirdco/flock.js',
|
||||
endpoint: 'https://api.tinybird.co',
|
||||
token: 'tinybird_token',
|
||||
id: 'tb_test_site_uuid'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
it('with all tb_variables set to undefined on logged out home page', async function () {
|
||||
await testGhostHead(testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('Sets tb_post_uuid on post page', async function () {
|
||||
const renderObject = {
|
||||
post: posts[10]
|
||||
};
|
||||
|
||||
await testGhostHead(testUtils.createHbsResponse({
|
||||
renderObject: renderObject,
|
||||
locals: {
|
||||
relativeUrl: '/post/',
|
||||
context: ['post'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('sets tb_member_x variables on logged in home page', async function () {
|
||||
const renderObject = {
|
||||
member: {
|
||||
uuid: 'member_uuid',
|
||||
status: 'paid'
|
||||
}
|
||||
};
|
||||
|
||||
await testGhostHead(testUtils.createHbsResponse({
|
||||
renderObject: renderObject,
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('sets both tb_member_x variables and tb_post_uuid on logged in post page', async function () {
|
||||
const renderObject = {
|
||||
member: {
|
||||
uuid: 'member_uuid',
|
||||
status: 'free'
|
||||
},
|
||||
post: posts[10]
|
||||
};
|
||||
|
||||
await testGhostHead(testUtils.createHbsResponse({
|
||||
renderObject: renderObject,
|
||||
locals: {
|
||||
relativeUrl: '/post/',
|
||||
context: ['post'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -495,10 +495,14 @@ class StaffServiceEmails {
|
||||
sharedData = await this.getSharedData(data.recipient);
|
||||
}
|
||||
|
||||
return htmlTemplate({
|
||||
const html = htmlTemplate({
|
||||
...data,
|
||||
...sharedData
|
||||
});
|
||||
|
||||
const juice = require('juice');
|
||||
|
||||
return juice(html, {inlinePseudoElements: true, removeStyleTags: true});
|
||||
}
|
||||
|
||||
async renderText(templateName, data) {
|
||||
|
@ -37,9 +37,9 @@
|
||||
<tr>
|
||||
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.name}} {{#if memberData}}• <a href="{{memberData.adminUrl}}" target="_blank" style="display: inline; color: {{accentColor}} !important; text-decoration: none !important;">View</a>{{/if}}</p>
|
||||
<p class="text-link-accent-large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.name}} {{#if memberData}}• <a href="{{memberData.adminUrl}}" target="_blank" style="display: inline; color: {{accentColor}} !important; text-decoration: none !important;">View</a>{{/if}}</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
|
||||
<p class="large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
|
||||
{{#if donation.donationMessage}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 22px; padding: 0; padding-bottom: 28px; text-align: left; margin: 0; color: #15171A; font-weight: 400;">“{{donation.donationMessage}}”</p>
|
||||
{{/if}}
|
||||
|
@ -42,13 +42,13 @@
|
||||
<tr>
|
||||
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Name:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
{{#if referrerSource}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Source:</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{referrerSource}}</p>
|
||||
<p class="large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{referrerSource}}</p>
|
||||
{{#if attributionTitle}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Page:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;"><a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;"><a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
|
@ -46,15 +46,15 @@
|
||||
<tr>
|
||||
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Name:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Tier:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{tierData.name}}{{#if tierData.details}} • {{tierData.details}}{{/if}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{tierData.name}}{{#if tierData.details}} • {{tierData.details}}{{/if}}</p>
|
||||
{{#if subscriptionData.cancelNow}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Expired on:</p>
|
||||
{{else}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Expires on:</p>
|
||||
{{/if}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{subscriptionData.expiryAt}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{subscriptionData.expiryAt}}</p>
|
||||
{{#if subscriptionData.cancellationReason}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 400;">"{{subscriptionData.cancellationReason}}"</p>
|
||||
{{/if}}
|
||||
|
@ -42,19 +42,19 @@
|
||||
<tr>
|
||||
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Name:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Tier:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{tierData.name}}{{#if tierData.details}} • {{tierData.details}}{{/if}}</p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{tierData.name}}{{#if tierData.details}} • {{tierData.details}}{{/if}}</p>
|
||||
{{#if offerData}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Offer:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{offerData.name}} • <span style="color: {{accentColor}};">{{offerData.details}}</span></p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{offerData.name}} • <span style="color: {{accentColor}};">{{offerData.details}}</span></p>
|
||||
{{/if}}
|
||||
{{#if referrerSource}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Source:</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{referrerSource}}</p>
|
||||
<p class="large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{referrerSource}}</p>
|
||||
{{#if attributionTitle}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Page:</p>
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;"><a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
|
||||
<p class="text-link large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;"><a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
|
@ -3,48 +3,46 @@
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
table.body h1 {
|
||||
font-size: 22px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
table.body p,
|
||||
table.body ul,
|
||||
table.body ol,
|
||||
table.body td,
|
||||
table.body span,
|
||||
table.body a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
table.body .wrapper,
|
||||
table.body .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
table.body .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
table.body .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
table.body .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
table.body .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
table[class=body] p[class=small],
|
||||
table[class=body] a[class=small] {
|
||||
font-size: 11px !important;
|
||||
table.body p.large,
|
||||
table.body p.large a {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
table.body p.small,
|
||||
table.body a.small {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.new-mention-thumbnail {
|
||||
display: none !important;
|
||||
@ -74,6 +72,13 @@
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.text-link-accent a {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
@ -23,9 +23,10 @@
|
||||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
"@tryghost/email-addresses": "0.0.0",
|
||||
"handlebars": "4.7.8",
|
||||
"@tryghost/email-addresses": "0.0.0"
|
||||
"juice": "9.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1"
|
||||
}
|
||||
}
|
||||
|
31
ghost/tinybird/.tinyenv
Normal file
31
ghost/tinybird/.tinyenv
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
# VERSION format is major.minor.patch-post where major, minor, patch and post are integer numbers
|
||||
# bump post to deploy to the current live Release, rollback to previous post version is not available
|
||||
# bump patch or minor to deploy a new Release and auto-promote it to live. Add TB_AUTO_PROMOTE=0 to create the Release in preview status
|
||||
# bump major to deploy a new Release in preview status
|
||||
VERSION=0.0.0
|
||||
|
||||
|
||||
|
||||
##########
|
||||
# OPTIONAL env vars
|
||||
# Deploy a new Release in preview status (default is 1)
|
||||
# TB_AUTO_PROMOTE=0
|
||||
|
||||
# Check if deploy requires backfilling on preview (default is 1)
|
||||
# TB_CHECK_BACKFILL_REQUIRED=0
|
||||
|
||||
# Force old Releases deletion on promote (default is 0)
|
||||
# Setting it to 1 will remove oldest rollback Releases even when some resource is still in use
|
||||
# TB_FORCE_REMOVE_OLDEST_ROLLBACK=0
|
||||
|
||||
# Don't print CLI version warning message if there's a new available version
|
||||
# TB_VERSION_WARNING=0
|
||||
|
||||
# Skip regression tests
|
||||
# TB_SKIP_REGRESSION=0
|
||||
|
||||
# Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
|
||||
# OBFUSCATE_REGEX_PATTERN="https://(www\.)?[^/]+||^Follow these instructions =>"
|
||||
# OBFUSCATE_PATTERN_SEPARATOR=||
|
||||
##########
|
18
ghost/tinybird/README.md
Normal file
18
ghost/tinybird/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Tinybird
|
||||
|
||||
This folder contains configuration for Tinybird, so that the stats feature can be used.
|
||||
|
||||
We sync this configuration with Tinybird via the Tinybird CLI.
|
||||
|
||||
## Tinybird CLI
|
||||
|
||||
The Tinybird CLI is used via Docker.
|
||||
|
||||
```bash
|
||||
yarn tb
|
||||
```
|
||||
|
||||
Documentation for the Tinybird CLI: https://docs.tinybird.co/v/0.22.0/cli/overview
|
||||
Note: you can use python if you prefer, but we use Docker for consistency.
|
||||
|
||||
How to work with version control: https://docs.tinybird.co/v/0.22.0/guides/version-control
|
14
ghost/tinybird/datasources/analytics_events.datasource
Normal file
14
ghost/tinybird/datasources/analytics_events.datasource
Normal file
@ -0,0 +1,14 @@
|
||||
DESCRIPTION >
|
||||
Analytics events landing data source
|
||||
|
||||
SCHEMA >
|
||||
`timestamp` DateTime `json:$.timestamp`,
|
||||
`session_id` String `json:$.session_id`,
|
||||
`action` LowCardinality(String) `json:$.action`,
|
||||
`version` LowCardinality(String) `json:$.version`,
|
||||
`payload` String `json:$.payload`
|
||||
|
||||
ENGINE MergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(timestamp)
|
||||
ENGINE_SORTING_KEY timestamp
|
||||
ENGINE_TTL timestamp + toIntervalDay(60)
|
18
ghost/tinybird/datasources/analytics_pages_mv.datasource
Normal file
18
ghost/tinybird/datasources/analytics_pages_mv.datasource
Normal file
@ -0,0 +1,18 @@
|
||||
SCHEMA >
|
||||
`site_uuid` String,
|
||||
`member_uuid` String,
|
||||
`member_status` String,
|
||||
`post_uuid` String,
|
||||
`date` Date,
|
||||
`device` String,
|
||||
`browser` String,
|
||||
`location` String,
|
||||
`pathname` String,
|
||||
`visits` AggregateFunction(uniq, String),
|
||||
`hits` AggregateFunction(count),
|
||||
`logged_in_hits` AggregateFunction(count),
|
||||
`logged_out_hits` AggregateFunction(count)
|
||||
|
||||
ENGINE AggregatingMergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(date)
|
||||
ENGINE_SORTING_KEY date, device, browser, location, pathname, member_uuid, member_status, post_uuid, site_uuid
|
14
ghost/tinybird/datasources/analytics_sessions_mv.datasource
Normal file
14
ghost/tinybird/datasources/analytics_sessions_mv.datasource
Normal file
@ -0,0 +1,14 @@
|
||||
SCHEMA >
|
||||
`site_uuid` String,
|
||||
`date` Date,
|
||||
`session_id` String,
|
||||
`device` SimpleAggregateFunction(any, String),
|
||||
`browser` SimpleAggregateFunction(any, String),
|
||||
`location` SimpleAggregateFunction(any, String),
|
||||
`first_hit` SimpleAggregateFunction(min, DateTime),
|
||||
`latest_hit` SimpleAggregateFunction(max, DateTime),
|
||||
`hits` AggregateFunction(count)
|
||||
|
||||
ENGINE AggregatingMergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(date)
|
||||
ENGINE_SORTING_KEY date, session_id, site_uuid
|
13
ghost/tinybird/datasources/analytics_sources_mv.datasource
Normal file
13
ghost/tinybird/datasources/analytics_sources_mv.datasource
Normal file
@ -0,0 +1,13 @@
|
||||
SCHEMA >
|
||||
`site_uuid` String,
|
||||
`date` Date,
|
||||
`device` String,
|
||||
`browser` String,
|
||||
`location` String,
|
||||
`referrer` String,
|
||||
`visits` AggregateFunction(uniq, String),
|
||||
`hits` AggregateFunction(count)
|
||||
|
||||
ENGINE AggregatingMergeTree
|
||||
ENGINE_PARTITION_KEY toYYYYMM(date)
|
||||
ENGINE_SORTING_KEY date, device, browser, location, referrer, site_uuid
|
15
ghost/tinybird/datasources/fixtures/README.md
Normal file
15
ghost/tinybird/datasources/fixtures/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Datasource fixtures
|
||||
|
||||
The file mockingbird-schema.json is a schema for generating fake data using the Mockingbird CLI.
|
||||
|
||||
The CLI is installed via npm:
|
||||
|
||||
```
|
||||
npm install -g @tinybirdco/mockingbird
|
||||
```
|
||||
|
||||
The command I'm currently using to generate the data is:
|
||||
|
||||
```
|
||||
mockingbird-cli tinybird --schema ghost/tinybird/datasources/fixtures/mockingbird-schema.json --endpoint gcp_europe_west3 --token xxxx --datasource analytics_events --eps 50 --limit 5000
|
||||
```
|
72
ghost/tinybird/datasources/fixtures/mockingbird-schema.json
Normal file
72
ghost/tinybird/datasources/fixtures/mockingbird-schema.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"timestamp": {
|
||||
"type": "mockingbird.datetimeBetween",
|
||||
"params": [
|
||||
{
|
||||
"start": "2024-07-01T00:00:00.000Z",
|
||||
"end": "2024-08-20T12:00:00.000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string.uuid"
|
||||
},
|
||||
"action": {
|
||||
"type": "mockingbird.pick",
|
||||
"params": [
|
||||
{
|
||||
"values": [
|
||||
"page_hit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "mockingbird.pick",
|
||||
"params": [
|
||||
{
|
||||
"values": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "mockingbird.pickWeighted",
|
||||
"params": [
|
||||
{
|
||||
"values": [
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\", \"locale\":\"en-US\", \"referrer\":\"https://www.kike.io\", \"pathname\":\"/coming-soon/\", \"href\":\"https://web-analytics.ghost.is/coming-soon/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/104.0.5112.79 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"IT\", \"referrer\":\"https://www.hn.com\", \"pathname\":\"/about/\", \"href\":\"https://web-analytics.ghost.is/about/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0\", \"locale\":\"en-GB\", \"location\":\"ES\", \"referrer\":\"\", \"pathname\":\"/\", \"href\":\"https://web-analytics.ghost.is\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/\", \"href\":\"https://web-analytics.ghost.is\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://web-analytics.ghost.is/\", \"pathname\":\"/coming-soon/\", \"href\":\"https://web-analytics.ghost.is/coming-soon/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/hello-world/\", \"href\":\"https://web-analytics.ghost.is/hello-world/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"IL\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/hello-world/\", \"href\":\"https://web-analytics.ghost.is/hello-world/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1\", \"locale\":\"es-ES\", \"location\":\"ES\", \"referrer\":\"https://www.twitter.com\", \"pathname\":\"/\", \"href\":\"https://web-analytics.ghost.is/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"GB\", \"referrer\":\"https://www.facebook.com\", \"pathname\":\"/\", \"href\":\"https://web-analytics.ghost.is/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"CH\", \"referrer\":\"https://www.qq.ch\", \"pathname\":\"/coming-soon/\", \"href\":\"https://web-analytics.ghost.is/coming-soon/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.118 Mobile Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.yandex.com\", \"pathname\":\"/about/\", \"href\":\"https://web-analytics.ghost.is/about/\"}",
|
||||
"{\"site_uuid\":\"mock_site_uuid\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 13; SM-A102U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.118 Mobile Safari/537.36\", \"locale\":\"en-US\", \"location\":\"FR\", \"referrer\":\"https://www.github.com\", \"pathname\":\"/coming-soon/\", \"href\":\"https://web-analytics.ghost.is/coming-soon/\"}",
|
||||
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\", \"locale\":\"en-US\", \"referrer\":\"https://www.kike.io\", \"pathname\":\"/products/\", \"href\":\"https://fake-site.ghost.is/products/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/104.0.5112.79 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"IT\", \"referrer\":\"https://www.hn.com\", \"pathname\":\"/blog/\", \"href\":\"https://fake-site.ghost.is/blog/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0\", \"locale\":\"en-GB\", \"location\":\"ES\", \"referrer\":\"\", \"pathname\":\"/contact/\", \"href\":\"https://fake-site.ghost.is/contact/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/faq/\", \"href\":\"https://fake-site.ghost.is/faq/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://fake-site.ghost.is/\", \"pathname\":\"/services/\", \"href\":\"https://fake-site.ghost.is/services/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/team/\", \"href\":\"https://fake-site.ghost.is/team/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"IL\", \"referrer\":\"https://www.google.com\", \"pathname\":\"/pricing/\", \"href\":\"https://fake-site.ghost.is/pricing/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1\", \"locale\":\"es-ES\", \"location\":\"ES\", \"referrer\":\"https://www.twitter.com\", \"pathname\":\"/resources/\", \"href\":\"https://fake-site.ghost.is/resources/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"GB\", \"referrer\":\"https://www.facebook.com\", \"pathname\":\"/careers/\", \"href\":\"https://fake-site.ghost.is/careers/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\", \"locale\":\"en-US\", \"location\":\"CH\", \"referrer\":\"https://www.qq.ch\", \"pathname\":\"/support/\", \"href\":\"https://fake-site.ghost.is/support/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.118 Mobile Safari/537.36\", \"locale\":\"en-US\", \"location\":\"US\", \"referrer\":\"https://www.yandex.com\", \"pathname\":\"/partners/\", \"href\":\"https://fake-site.ghost.is/partners/\"}",
|
||||
"{\"site_uuid\":\"fake_site_id\", \"user-agent\":\"Mozilla/5.0 (Linux; Android 13; SM-A102U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.118 Mobile Safari/537.36\", \"locale\":\"en-US\", \"location\":\"FR\", \"referrer\":\"https://www.github.com\", \"pathname\":\"/events/\", \"href\":\"https://fake-site.ghost.is/events/\"}"
|
||||
],
|
||||
"weights": [
|
||||
200, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 400,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
71
ghost/tinybird/pipes/analytics_hits.pipe
Normal file
71
ghost/tinybird/pipes/analytics_hits.pipe
Normal file
@ -0,0 +1,71 @@
|
||||
DESCRIPTION >
|
||||
Parsed `page_hit` events, implementing `browser` and `device` detection logic.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE parsed_hits
|
||||
DESCRIPTION >
|
||||
Parse raw page_hit events
|
||||
|
||||
SQL >
|
||||
SELECT
|
||||
timestamp,
|
||||
action,
|
||||
version,
|
||||
coalesce(session_id, '0') as session_id,
|
||||
JSONExtractString(payload, 'locale') as locale,
|
||||
JSONExtractString(payload, 'location') as location,
|
||||
JSONExtractString(payload, 'referrer') as referrer,
|
||||
JSONExtractString(payload, 'pathname') as pathname,
|
||||
JSONExtractString(payload, 'href') as href,
|
||||
JSONExtractString(payload, 'site_uuid') as site_uuid,
|
||||
JSONExtractString(payload, 'member_uuid') as member_uuid,
|
||||
JSONExtractString(payload, 'member_status') as member_status,
|
||||
JSONExtractString(payload, 'post_uuid') as post_uuid,
|
||||
lower(JSONExtractString(payload, 'user-agent')) as user_agent
|
||||
FROM analytics_events
|
||||
where action = 'page_hit'
|
||||
|
||||
NODE endpoint
|
||||
SQL >
|
||||
SELECT
|
||||
site_uuid,
|
||||
timestamp,
|
||||
action,
|
||||
version,
|
||||
session_id,
|
||||
case
|
||||
when member_uuid REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
then true
|
||||
else false
|
||||
END as logged_in,
|
||||
member_uuid,
|
||||
member_status,
|
||||
post_uuid,
|
||||
location,
|
||||
referrer,
|
||||
pathname,
|
||||
href,
|
||||
case
|
||||
when match(user_agent, 'wget|ahrefsbot|curl|urllib|bitdiscovery|\+https://|googlebot')
|
||||
then 'bot'
|
||||
when match(user_agent, 'android')
|
||||
then 'mobile-android'
|
||||
when match(user_agent, 'ipad|iphone|ipod')
|
||||
then 'mobile-ios'
|
||||
else 'desktop'
|
||||
END as device,
|
||||
case
|
||||
when match(user_agent, 'firefox')
|
||||
then 'firefox'
|
||||
when match(user_agent, 'chrome|crios')
|
||||
then 'chrome'
|
||||
when match(user_agent, 'opera')
|
||||
then 'opera'
|
||||
when match(user_agent, 'msie|trident')
|
||||
then 'ie'
|
||||
when match(user_agent, 'iphone|ipad|safari')
|
||||
then 'safari'
|
||||
else 'Unknown'
|
||||
END as browser
|
||||
FROM parsed_hits
|
24
ghost/tinybird/pipes/analytics_pages.pipe
Normal file
24
ghost/tinybird/pipes/analytics_pages.pipe
Normal file
@ -0,0 +1,24 @@
|
||||
NODE analytics_pages_1
|
||||
DESCRIPTION >
|
||||
Aggregate by pathname and calculate session and hits
|
||||
|
||||
SQL >
|
||||
SELECT
|
||||
site_uuid,
|
||||
member_uuid,
|
||||
member_status,
|
||||
post_uuid,
|
||||
toDate(timestamp) AS date,
|
||||
device,
|
||||
browser,
|
||||
location,
|
||||
pathname,
|
||||
uniqState(session_id) AS visits,
|
||||
countState() AS hits,
|
||||
countStateIf(logged_in = true) AS logged_in_hits,
|
||||
countStateIf(logged_in = false) AS logged_out_hits
|
||||
FROM analytics_hits
|
||||
GROUP BY date, device, browser, location, pathname, member_uuid, member_status, post_uuid, site_uuid
|
||||
|
||||
TYPE MATERIALIZED
|
||||
DATASOURCE analytics_pages_mv
|
20
ghost/tinybird/pipes/analytics_sessions.pipe
Normal file
20
ghost/tinybird/pipes/analytics_sessions.pipe
Normal file
@ -0,0 +1,20 @@
|
||||
NODE analytics_sessions_1
|
||||
DESCRIPTION >
|
||||
Aggregate by session_id and calculate session metrics
|
||||
|
||||
SQL >
|
||||
SELECT
|
||||
site_uuid,
|
||||
toDate(timestamp) AS date,
|
||||
session_id,
|
||||
anySimpleState(device) AS device,
|
||||
anySimpleState(browser) AS browser,
|
||||
anySimpleState(location) AS location,
|
||||
minSimpleState(timestamp) AS first_hit,
|
||||
maxSimpleState(timestamp) AS latest_hit,
|
||||
countState() AS hits
|
||||
FROM analytics_hits
|
||||
GROUP BY date, session_id, site_uuid
|
||||
|
||||
TYPE MATERIALIZED
|
||||
DATASOURCE analytics_sessions_mv
|
21
ghost/tinybird/pipes/analytics_sources.pipe
Normal file
21
ghost/tinybird/pipes/analytics_sources.pipe
Normal file
@ -0,0 +1,21 @@
|
||||
NODE analytics_sources_1
|
||||
DESCRIPTION >
|
||||
Aggregate by referral and calculate session and hits
|
||||
|
||||
SQL >
|
||||
WITH (SELECT domainWithoutWWW(href) FROM analytics_hits LIMIT 1) AS current_domain
|
||||
SELECT
|
||||
site_uuid,
|
||||
toDate(timestamp) AS date,
|
||||
device,
|
||||
browser,
|
||||
location,
|
||||
referrer,
|
||||
uniqState(session_id) AS visits,
|
||||
countState() AS hits
|
||||
FROM analytics_hits
|
||||
WHERE domainWithoutWWW(referrer) != current_domain
|
||||
GROUP BY date, device, browser, location, referrer, site_uuid
|
||||
|
||||
TYPE MATERIALIZED
|
||||
DATASOURCE analytics_sources_mv
|
130
ghost/tinybird/pipes/kpis.pipe
Normal file
130
ghost/tinybird/pipes/kpis.pipe
Normal file
@ -0,0 +1,130 @@
|
||||
DESCRIPTION >
|
||||
Summary with general KPIs per date, including visits, page views, bounce rate and average session duration.
|
||||
Accepts `date_from` and `date_to` date filter, all historical data if not passed.
|
||||
Daily granularity, except when filtering one single day (hourly)
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE timeseries
|
||||
DESCRIPTION >
|
||||
Generate a timeseries for the specified time range, so we call fill empty data points.
|
||||
Filters "future" data points.
|
||||
|
||||
SQL >
|
||||
%
|
||||
{% set _single_day = defined(date_from) and day_diff(date_from, date_to) == 0 %}
|
||||
with
|
||||
{% if defined(date_from) %}
|
||||
toStartOfDay(
|
||||
toDate(
|
||||
{{
|
||||
Date(
|
||||
date_from,
|
||||
description="Starting day for filtering a date range",
|
||||
required=False,
|
||||
)
|
||||
}}
|
||||
)
|
||||
) as start,
|
||||
{% else %} toStartOfDay(timestampAdd(today(), interval -7 day)) as start,
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
toStartOfDay(
|
||||
toDate(
|
||||
{{
|
||||
Date(
|
||||
date_to,
|
||||
description="Finishing day for filtering a date range",
|
||||
required=False,
|
||||
)
|
||||
}}
|
||||
)
|
||||
) as end
|
||||
{% else %} toStartOfDay(today()) as end
|
||||
{% end %}
|
||||
{% if _single_day %}
|
||||
select
|
||||
arrayJoin(
|
||||
arrayMap(
|
||||
x -> toDateTime(x),
|
||||
range(
|
||||
toUInt32(toDateTime(start)), toUInt32(timestampAdd(end, interval 1 day)), 3600
|
||||
)
|
||||
)
|
||||
) as date
|
||||
{% else %}
|
||||
select
|
||||
arrayJoin(
|
||||
arrayMap(
|
||||
x -> toDate(x),
|
||||
range(toUInt32(start), toUInt32(timestampAdd(end, interval 1 day)), 24 * 3600)
|
||||
)
|
||||
) as date
|
||||
{% end %}
|
||||
where date <= now()
|
||||
|
||||
NODE hits
|
||||
DESCRIPTION >
|
||||
Group by sessions and calculate metrics at that level
|
||||
|
||||
SQL >
|
||||
%
|
||||
{% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
|
||||
select
|
||||
site_uuid,
|
||||
toStartOfHour(timestamp) as date,
|
||||
session_id,
|
||||
uniq(session_id) as visits,
|
||||
count() as pageviews,
|
||||
case when min(timestamp) = max(timestamp) then 1 else 0 end as is_bounce,
|
||||
max(timestamp) as latest_hit_aux,
|
||||
min(timestamp) as first_hit_aux
|
||||
from analytics_hits
|
||||
where toDate(timestamp) = {{ Date(date_from) }}
|
||||
group by toStartOfHour(timestamp), session_id, site_uuid
|
||||
{% else %}
|
||||
select
|
||||
site_uuid,
|
||||
date,
|
||||
session_id,
|
||||
uniq(session_id) as visits,
|
||||
countMerge(hits) as pageviews,
|
||||
case when min(first_hit) = max(latest_hit) then 1 else 0 end as is_bounce,
|
||||
max(latest_hit) as latest_hit_aux,
|
||||
min(first_hit) as first_hit_aux
|
||||
from analytics_sessions_mv
|
||||
where
|
||||
{% if defined(date_from) %} date >= {{ Date(date_from) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %} and date <= {{ Date(date_to) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by date, session_id, site_uuid
|
||||
{% end %}
|
||||
|
||||
NODE data
|
||||
DESCRIPTION >
|
||||
General KPIs per date, works for both summary metrics and trends charts.
|
||||
|
||||
SQL >
|
||||
select
|
||||
site_uuid,
|
||||
date,
|
||||
uniq(session_id) as visits,
|
||||
sum(pageviews) as pageviews,
|
||||
sum(case when latest_hit_aux = first_hit_aux then 1 end) / visits as bounce_rate,
|
||||
avg(latest_hit_aux - first_hit_aux) as avg_session_sec
|
||||
from hits
|
||||
group by date, site_uuid
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Join and generate timeseries with metrics
|
||||
|
||||
SQL >
|
||||
%
|
||||
select a.date, b.visits, b.pageviews, b.bounce_rate, b.avg_session_sec
|
||||
from timeseries a
|
||||
left join data b using date
|
||||
where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}}
|
32
ghost/tinybird/pipes/top_browsers.pipe
Normal file
32
ghost/tinybird/pipes/top_browsers.pipe
Normal file
@ -0,0 +1,32 @@
|
||||
DESCRIPTION >
|
||||
Top Browsers ordered by most visits.
|
||||
Accepts `date_from` and `date_to` date filter. Defaults to last 7 days.
|
||||
Also `skip` and `limit` parameters for pagination.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Group by browser and calculate hits and visits
|
||||
|
||||
SQL >
|
||||
%
|
||||
select browser, uniqMerge(visits) as visits, countMerge(hits) as hits
|
||||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
{% if defined(date_from) %}
|
||||
date
|
||||
>=
|
||||
{{ Date(date_from, description="Starting day for filtering a date range", required=False) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
and date
|
||||
<=
|
||||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by browser
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
33
ghost/tinybird/pipes/top_devices.pipe
Normal file
33
ghost/tinybird/pipes/top_devices.pipe
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
DESCRIPTION >
|
||||
Top Device Types ordered by most visits.
|
||||
Accepts `date_from` and `date_to` date filter. Defaults to last 7 days.
|
||||
Also `skip` and `limit` parameters for pagination.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Group by device and calculate hits and visits
|
||||
|
||||
SQL >
|
||||
%
|
||||
select device, uniqMerge(visits) as visits, countMerge(hits) as hits
|
||||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
{% if defined(date_from) %}
|
||||
date
|
||||
>=
|
||||
{{ Date(date_from, description="Starting day for filtering a date range", required=False) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
and date
|
||||
<=
|
||||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by device
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
32
ghost/tinybird/pipes/top_locations.pipe
Normal file
32
ghost/tinybird/pipes/top_locations.pipe
Normal file
@ -0,0 +1,32 @@
|
||||
DESCRIPTION >
|
||||
Top visiting Countries ordered by most visits.
|
||||
Accepts `date_from` and `date_to` date filter. Defaults to last 7 days.
|
||||
Also `skip` and `limit` parameters for pagination.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Group by pagepath and calculate hits and visits
|
||||
|
||||
SQL >
|
||||
%
|
||||
select location, uniqMerge(visits) as visits, countMerge(hits) as hits
|
||||
from analytics_pages_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
{% if defined(date_from) %}
|
||||
date
|
||||
>=
|
||||
{{ Date(date_from, description="Starting day for filtering a date range", required=False) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
and date
|
||||
<=
|
||||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by location
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
38
ghost/tinybird/pipes/top_pages.pipe
Normal file
38
ghost/tinybird/pipes/top_pages.pipe
Normal file
@ -0,0 +1,38 @@
|
||||
DESCRIPTION >
|
||||
Most visited pages for a given period.
|
||||
Accepts `date_from` and `date_to` date filter. Defaults to last 7 days.
|
||||
Also `skip` and `limit` parameters for pagination.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Group by pagepath and calculate hits and visits
|
||||
|
||||
SQL >
|
||||
%
|
||||
select
|
||||
pathname,
|
||||
uniqMerge(visits) as visits,
|
||||
countMerge(hits) as hits,
|
||||
countMerge(logged_in_hits) as logged_in_hits,
|
||||
countMerge(logged_out_hits) as logged_out_hits
|
||||
from analytics_pages_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
{% if defined(date_from) %}
|
||||
date
|
||||
>=
|
||||
{{ Date(date_from, description="Starting day for filtering a date range", required=False) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
and date
|
||||
<=
|
||||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
|
||||
group by pathname
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
33
ghost/tinybird/pipes/top_sources.pipe
Normal file
33
ghost/tinybird/pipes/top_sources.pipe
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
DESCRIPTION >
|
||||
Top traffic sources (domains), ordered by most visits.
|
||||
Accepts `date_from` and `date_to` date filter. Defaults to last 7 days.
|
||||
Also `skip` and `limit` parameters for pagination.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Group by referral and calculate hits and visits
|
||||
|
||||
SQL >
|
||||
%
|
||||
select domainWithoutWWW(referrer) as referrer, uniqMerge(visits) as visits, countMerge(hits) as hits
|
||||
from analytics_sources_mv
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
{% if defined(date_from) %}
|
||||
date
|
||||
>=
|
||||
{{ Date(date_from, description="Starting day for filtering a date range", required=False) }}
|
||||
{% else %} date >= timestampAdd(today(), interval -7 day)
|
||||
{% end %}
|
||||
{% if defined(date_to) %}
|
||||
and date
|
||||
<=
|
||||
{{ Date(date_to, description="Finishing day for filtering a date range", required=False) }}
|
||||
{% else %} and date <= today()
|
||||
{% end %}
|
||||
group by referrer
|
||||
order by visits desc
|
||||
limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }}
|
36
ghost/tinybird/pipes/trend.pipe
Normal file
36
ghost/tinybird/pipes/trend.pipe
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
DESCRIPTION >
|
||||
Visits trend over time for the last 30 minutes, filling the blanks.
|
||||
Works great for the realtime chart.
|
||||
|
||||
TOKEN "dashboard" READ
|
||||
|
||||
NODE timeseries
|
||||
DESCRIPTION >
|
||||
Generate a timeseries for the last 30 minutes, so we call fill empty data points
|
||||
|
||||
SQL >
|
||||
with (now() - interval 30 minute) as start
|
||||
select addMinutes(toStartOfMinute(start), number) as t
|
||||
from (select arrayJoin(range(1, 31)) as number)
|
||||
|
||||
NODE hits
|
||||
DESCRIPTION >
|
||||
Get last 30 minutes metrics gropued by minute
|
||||
|
||||
SQL >
|
||||
%
|
||||
select toStartOfMinute(timestamp) as t, uniq(session_id) as visits
|
||||
from analytics_hits
|
||||
where
|
||||
site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} and
|
||||
timestamp >= (now() - interval 30 minute)
|
||||
group by toStartOfMinute(timestamp)
|
||||
order by toStartOfMinute(timestamp)
|
||||
|
||||
NODE endpoint
|
||||
DESCRIPTION >
|
||||
Join and generate timeseries with metrics for the last 30 minutes
|
||||
|
||||
SQL >
|
||||
select a.t, b.visits from timeseries a left join hits b on a.t = b.t order by a.t
|
1
ghost/tinybird/requirements.txt
Normal file
1
ghost/tinybird/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
tinybird-cli>=4,<5
|
21
ghost/tinybird/scripts/append_fixtures.sh
Executable file
21
ghost/tinybird/scripts/append_fixtures.sh
Executable file
@ -0,0 +1,21 @@
|
||||
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
directory="datasources/fixtures"
|
||||
extensions=("csv" "ndjson")
|
||||
|
||||
absolute_directory=$(realpath "$directory")
|
||||
|
||||
for extension in "${extensions[@]}"; do
|
||||
file_list=$(find "$absolute_directory" -type f -name "*.$extension")
|
||||
|
||||
for file_path in $file_list; do
|
||||
file_name=$(basename "$file_path")
|
||||
file_name_without_extension="${file_name%.*}"
|
||||
|
||||
command="tb datasource append $file_name_without_extension datasources/fixtures/$file_name"
|
||||
echo $command
|
||||
$command
|
||||
done
|
||||
done
|
58
ghost/tinybird/scripts/exec_test.sh
Executable file
58
ghost/tinybird/scripts/exec_test.sh
Executable file
@ -0,0 +1,58 @@
|
||||
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
export TB_VERSION_WARNING=0
|
||||
|
||||
run_test() {
|
||||
t=$1
|
||||
echo "** Running $t **"
|
||||
echo "** $(cat $t)"
|
||||
tmpfile=$(mktemp)
|
||||
retries=0
|
||||
TOTAL_RETRIES=3
|
||||
|
||||
# When appending fixtures, we need to retry in case of the data is not replicated in time
|
||||
while [ $retries -lt $TOTAL_RETRIES ]; do
|
||||
# Run the test and store the output in a temporary file
|
||||
bash $t $2 >$tmpfile
|
||||
exit_code=$?
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
# If the test passed, break the loop
|
||||
if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
|
||||
break
|
||||
# If the test failed, increment the retries counter and try again
|
||||
else
|
||||
retries=$((retries+1))
|
||||
fi
|
||||
# If the bash command failed, print an error message and break the loop
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
|
||||
echo "✅ Test $t passed"
|
||||
rm $tmpfile
|
||||
return 0
|
||||
elif [ $retries -eq $TOTAL_RETRIES ]; then
|
||||
echo "🚨 ERROR: Test $t failed, diff:";
|
||||
diff -B ${t}.result $tmpfile
|
||||
rm $tmpfile
|
||||
return 1
|
||||
else
|
||||
echo "🚨 ERROR: Test $t failed with bash command exit code $?"
|
||||
cat $tmpfile
|
||||
rm $tmpfile
|
||||
return 1
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
export -f run_test
|
||||
|
||||
fail=0
|
||||
find ./tests -name "*.test" -print0 | xargs -0 -I {} -P 4 bash -c 'run_test "$@"' _ {} || fail=1
|
||||
|
||||
if [ $fail == 1 ]; then
|
||||
exit -1;
|
||||
fi
|
@ -40,7 +40,8 @@
|
||||
"main": "yarn main:monorepo && yarn main:submodules",
|
||||
"main:monorepo": "git checkout main && git pull ${GHOST_UPSTREAM:-origin} main && yarn",
|
||||
"main:submodules": "git submodule sync && git submodule update && git submodule foreach \"git checkout main && git pull ${GHOST_UPSTREAM:-origin} main && yarn\"",
|
||||
"prepare": "husky install .github/hooks"
|
||||
"prepare": "husky install .github/hooks",
|
||||
"tb": "docker run --rm -v $(pwd):/ghost -w /ghost/ghost/tinybird -it tinybirdco/tinybird-cli-docker"
|
||||
},
|
||||
"resolutions": {
|
||||
"@tryghost/errors": "1.3.5",
|
||||
|
Loading…
Reference in New Issue
Block a user