Merge branch 'main' into locales/hi

This commit is contained in:
Mandeep Singh 2024-08-30 21:30:11 +05:30 committed by GitHub
commit f295977bf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1555 additions and 516 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -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 = () => {

View File

@ -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}}

View File

@ -93,31 +93,30 @@ 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,
type: this.args.publishOptions.post.displayName
}));
if (this.args.publishOptions.post.displayName !== 'page') {
this.router.transitionTo('posts');
if (this.args.publishOptions.isScheduled) {
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
if (this.args.publishOptions.post.displayName !== 'page') {
this.router.transitionTo('posts');
} else {
this.router.transitionTo('pages');
}
} else {
localStorage.setItem('ghost-last-published-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
if (this.args.publishOptions.post.displayName !== 'page') {
if (this.args.publishOptions.post.hasEmail) {
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
} else {
this.router.transitionTo('pages');
this.router.transitionTo('posts');
}
} else {
localStorage.setItem('ghost-last-published-post', JSON.stringify({
id: this.args.publishOptions.post.id,
type: this.args.publishOptions.post.displayName
}));
if (this.args.publishOptions.post.displayName !== 'page') {
if (this.args.publishOptions.post.hasEmail) {
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
} else {
this.router.transitionTo('posts');
}
} else {
this.router.transitionTo('pages');
}
this.router.transitionTo('pages');
}
}
} catch (e) {

View File

@ -70,7 +70,7 @@
<img src="{{or this.post.featureImage this.post.twitterImage this.post.ogImage}}" alt="{{this.post.title}}">
</figure>
{{/if}}
<div class="modal-body">
<h2>{{this.post.title}}</h2>
{{#if this.post.excerpt}}
@ -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">

View File

@ -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

View File

@ -16,9 +16,7 @@ export default class PostsList extends Component {
constructor() {
super(...arguments);
if (this.feature.publishFlowEndScreen) {
this.checkPublishFlowModal();
}
this.checkPublishFlowModal();
}
async checkPublishFlowModal() {

View File

@ -34,66 +34,58 @@
{{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}}
@showIcon={{true}}
@idleIcon="reload"
@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>
</button>
{{/unless}}
<div style="display: flex; gap: 8px;">
<GhTaskButton
@buttonText="Refresh"
@task={{this.fetchPostTask}}
@showIcon={{true}}
@idleIcon="reload"
@successText="Refreshed"
@class="gh-btn gh-btn-icon refresh"
@successClass="gh-btn gh-btn-icon refresh" />
{{#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>
</button>
{{/unless}}
<span class="dropdown">
<GhDropdownButton
@dropdownName="analytics-actions-menu"
@classNames="gh-post-list-cta gh-btn-icon icon-only gh-btn-action-icon"
@title="Analytics Actions"
data-test-button="analytics-actions"
>
<span>
{{svg-jar "dotdotdot"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
<GhDropdown
@name="analytics-actions-menu"
@tagName="ul"
@classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
@closeOnClick={{true}}
>
<li>
<LinkTo class="edit-post" @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}}>Edit post</LinkTo>
</li>
<li>
<a class="view-browser" href="{{this.post.url}}" target="_blank" rel="noopener noreferrer">View in browser</a>
</li>
<li>
<button
type="button"
class="delete-post mr2"
{{on "click" this.confirmDeleteMember}}
data-test-button="delete-post"
>
<span class="red">Delete post</span>
</button>
</li>
</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}}
<span class="dropdown">
<GhDropdownButton
@dropdownName="analytics-actions-menu"
@classNames="gh-post-list-cta gh-btn-icon icon-only gh-btn-action-icon"
@title="Analytics Actions"
data-test-button="analytics-actions"
>
<span>
{{svg-jar "dotdotdot"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
<GhDropdown
@name="analytics-actions-menu"
@tagName="ul"
@classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
@closeOnClick={{true}}
>
<li>
<LinkTo class="edit-post" @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}}>Edit post</LinkTo>
</li>
<li>
<a class="view-browser" href="{{this.post.url}}" target="_blank" rel="noopener noreferrer">View in browser</a>
</li>
<li>
<button
type="button"
class="delete-post mr2"
{{on "click" this.confirmDeleteMember}}
data-test-button="delete-post"
>
<span class="red">Delete post</span>
</button>
</li>
</GhDropdown>
</span>
</div>
</div>
</div>
</GhCanvasHeader>

View File

@ -50,9 +50,7 @@ export default class Analytics extends Component {
constructor() {
super(...arguments);
if (this.feature.publishFlowEndScreen) {
this.checkPublishFlowModal();
}
this.checkPublishFlowModal();
}
openPublishFlowModal() {
@ -73,11 +71,7 @@ export default class Analytics extends Component {
}
get post() {
if (this.feature.publishFlowEndScreen) {
return this._post ?? this.args.post;
}
return this.args.post;
return this._post ?? this.args.post;
}
set post(value) {

View File

@ -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')
};

View File

@ -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;

View File

@ -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",
@ -207,4 +207,4 @@
}
}
}
}
}

View File

@ -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');

View File

@ -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');

View File

@ -45,9 +45,7 @@ const ALPHA_FEATURES = [
'importMemberTier',
'lexicalIndicators',
'adminXDemo',
'contentVisibility',
'publishFlowEndScreen',
'postAnalyticsRefresh'
'contentVisibility'
];
module.exports.GA_KEYS = [...GA_FEATURES];

View File

@ -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",

View File

@ -29,8 +29,6 @@ Object {
"members": true,
"newEmailAddresses": true,
"outboundLinkTagging": true,
"postAnalyticsRefresh": true,
"publishFlowEndScreen": true,
"stripeAutomaticTax": true,
"themeErrorsNotification": true,
"tipsAndDonations": true,

View File

@ -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');

View File

@ -8,114 +8,102 @@ 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;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=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 {
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;
}
.new-mention-thumbnail {
display: none !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.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;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
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>
@media only screen and (max-width: 620px) {
table.body h1 {
font-size: 22px !important;
padding-bottom: 16px !important;
}
table.body p,
table.body ul,
table.body ol,
table.body td,
table.body span,
table.body a {
font-size: 16px !important;
}
table.body .wrapper,
table.body .article {
padding: 10px !important;
}
table.body .content {
padding: 0 !important;
}
table.body .container {
padding: 0 !important;
width: 100% !important;
}
table.body .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table.body .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !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;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.text-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
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;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</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,114 +252,102 @@ 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;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=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 {
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;
}
.new-mention-thumbnail {
display: none !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.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;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
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>
@media only screen and (max-width: 620px) {
table.body h1 {
font-size: 22px !important;
padding-bottom: 16px !important;
}
table.body p,
table.body ul,
table.body ol,
table.body td,
table.body span,
table.body a {
font-size: 16px !important;
}
table.body .wrapper,
table.body .article {
padding: 10px !important;
}
table.body .content {
padding: 0 !important;
}
table.body .container {
padding: 0 !important;
width: 100% !important;
}
table.body .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table.body .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !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;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.text-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
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;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</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,114 +475,102 @@ 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;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=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 {
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;
}
.new-mention-thumbnail {
display: none !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.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;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
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>
@media only screen and (max-width: 620px) {
table.body h1 {
font-size: 22px !important;
padding-bottom: 16px !important;
}
table.body p,
table.body ul,
table.body ol,
table.body td,
table.body span,
table.body a {
font-size: 16px !important;
}
table.body .wrapper,
table.body .article {
padding: 10px !important;
}
table.body .content {
padding: 0 !important;
}
table.body .container {
padding: 0 !important;
width: 100% !important;
}
table.body .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table.body .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !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;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.text-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
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;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</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>

View File

@ -457,7 +457,7 @@ Object {
"string": "<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\\" />
@ -469,7 +469,7 @@ Object {
<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\\",
@ -572,7 +572,7 @@ Object {
"string": "<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\\" />
@ -584,7 +584,7 @@ Object {
<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\\",
@ -686,7 +686,7 @@ Object {
"string": "<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\\" />
@ -698,7 +698,7 @@ Object {
<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\\",
@ -727,7 +727,7 @@ Object {
<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/npm/@tryghost/sodo-search@~1.0/umd/sodo-search.min.js\\" data-key=\\"xyz\\" data-styles=\\"https://cdn.jsdelivr.net/npm/@tryghost/sodo-search@~1.0/umd/main.css\\" data-sodo-search=\\"http://127.0.0.1:2369/\\" crossorigin=\\"anonymous\\"></script>",
}
`;
@ -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\\">

View File

@ -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'
}
}));
});
});
});

View File

@ -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) {

View File

@ -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}}&bull; <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}}&bull; <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}}

View File

@ -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>

View File

@ -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}} &bull; {{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}} &bull; {{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}}

View File

@ -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}} &bull; {{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}} &bull; {{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}} &bull; <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}} &bull; <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>

View File

@ -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;

View File

@ -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
View 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
View 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

View 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)

View 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

View 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

View 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

View 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
```

View 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
]
}
]
}
}

View 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

View 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

View 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

View 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

View 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)}}

View 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) }}

View 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) }}

View 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) }}

View 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) }}

View 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) }}

View 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

View File

@ -0,0 +1 @@
tinybird-cli>=4,<5

View 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

View 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

View File

@ -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",