mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
✨ Added "exclude" option for customizing {{ghost_head}} (#21229)
no ref {{ghost_head}} is huge, and some power-users and theme creators want the ability to customize what it contains. This PR makes it easier for a theme to write custom schema, or to load a custom version of portal/comments/search/etc, or to minimize load times by not loading scripts where they aren't needed, in a theme-specific way. Because ghost_head is controlled at the theme level, this gives folks in managed hosting the new ability to load a different version of the included app scripts (by preventing ghost_head from writing them and adding them in manually). Usage example: ` {{ghost_head exclude="search,portal"}} ` (empty array) No changes to current behavior search The built-in sodo-search script Includes adding the click event listener on buttons, generating the search index, and the UI. portal The portal script Handles sign-in and sign-up, payments, tips, memberships, etc, and all the portal data-attributes. announcement The announcement bar javascript If you'd like to use the announcement bar admin settings but not have it [mess up your CLS metric](https://www.spectralwebservices.com/blog/announcement-bar-a-review/), this is for you. metadata Skips HTML tags for meta description, favicon, canonical url, robots, referrer Important for SEO schema The LD+JSON schema Important for SEO card_assets Loads cards.min.css and .js Needed on any page with a post body, unless your theme replaces them all. Assets can also be selectively loaded with the [card_assets override](https://ghost.org/docs/themes/content/?ref=spectralwebservices.com#editor-cards) comment_counts Loads the comment_counts helper Needed if the page is using {{comments}} or data-ghost-comment-count attribute social_data Produces the og: and twitter: attributes for social media sharing and previews Required for good social media cards cta_styles Removes the call to action (CTA) styles Used for member signup and CTA cards - may be overwritten by your theme already
This commit is contained in:
parent
7e50a4051f
commit
f601ab3fda
3 changed files with 1254 additions and 56 deletions
|
@ -48,28 +48,31 @@ function finaliseStructuredData(meta) {
|
|||
return head;
|
||||
}
|
||||
|
||||
function getMembersHelper(data, frontendKey) {
|
||||
function getMembersHelper(data, frontendKey, excludeList) {
|
||||
// Do not load Portal if both Memberships and Tips & Donations and Recommendations are disabled
|
||||
if (!settingsCache.get('members_enabled') && !settingsCache.get('donations_enabled') && !settingsCache.get('recommendations_enabled')) {
|
||||
return '';
|
||||
}
|
||||
let membersHelper = '';
|
||||
if (!excludeList.has('portal')) {
|
||||
const {scriptUrl} = getFrontendAppConfig('portal');
|
||||
|
||||
const {scriptUrl} = getFrontendAppConfig('portal');
|
||||
|
||||
const colorString = (_.has(data, 'site._preview') && data.site.accent_color) ? data.site.accent_color : '';
|
||||
const attributes = {
|
||||
i18n: labs.isSet('i18n'),
|
||||
ghost: urlUtils.getSiteUrl(),
|
||||
key: frontendKey,
|
||||
api: urlUtils.urlFor('api', {type: 'content'}, true)
|
||||
};
|
||||
if (colorString) {
|
||||
attributes['accent-color'] = colorString;
|
||||
const colorString = (_.has(data, 'site._preview') && data.site.accent_color) ? data.site.accent_color : '';
|
||||
const attributes = {
|
||||
i18n: labs.isSet('i18n'),
|
||||
ghost: urlUtils.getSiteUrl(),
|
||||
key: frontendKey,
|
||||
api: urlUtils.urlFor('api', {type: 'content'}, true)
|
||||
};
|
||||
if (colorString) {
|
||||
attributes['accent-color'] = colorString;
|
||||
}
|
||||
const dataAttributes = getDataAttributes(attributes);
|
||||
membersHelper += `<script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>`;
|
||||
}
|
||||
if (!excludeList.has('cta_styles')) {
|
||||
membersHelper += (`<style id="gh-members-styles">${templateStyles}</style>`);
|
||||
}
|
||||
const dataAttributes = getDataAttributes(attributes);
|
||||
|
||||
let membersHelper = `<script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>`;
|
||||
membersHelper += (`<style id="gh-members-styles">${templateStyles}</style>`);
|
||||
if (settingsCache.get('paid_members_enabled')) {
|
||||
// disable fraud detection for e2e tests to reduce waiting time
|
||||
const isFraudSignalsEnabled = process.env.NODE_ENV === 'testing-browser' ? '?advancedFraudSignals=false' : '';
|
||||
|
@ -198,12 +201,11 @@ function getTinybirdTrackerScript(dataRoot) {
|
|||
// We use the name ghost_head to match the helper for consistency:
|
||||
module.exports = async function ghost_head(options) { // eslint-disable-line camelcase
|
||||
debug('begin');
|
||||
|
||||
// if server error page do nothing
|
||||
if (options.data.root.statusCode >= 500) {
|
||||
return;
|
||||
}
|
||||
|
||||
const excludeList = new Set(options?.hash?.exclude?.split(',') || []);
|
||||
const head = [];
|
||||
const dataRoot = options.data.root;
|
||||
const context = dataRoot._locals.context ? dataRoot._locals.context : null;
|
||||
|
@ -234,25 +236,26 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
|||
debug('end fetch');
|
||||
|
||||
if (context) {
|
||||
// head is our main array that holds our meta data
|
||||
if (meta.metaDescription && meta.metaDescription.length > 0) {
|
||||
head.push('<meta name="description" content="' + escapeExpression(meta.metaDescription) + '">');
|
||||
if (!excludeList.has('metadata')) {
|
||||
// head is our main array that holds our meta data
|
||||
if (meta.metaDescription && meta.metaDescription.length > 0) {
|
||||
head.push('<meta name="description" content="' + escapeExpression(meta.metaDescription) + '">');
|
||||
}
|
||||
|
||||
// no output in head if a publication icon is not set
|
||||
if (settingsCache.get('icon')) {
|
||||
head.push('<link rel="icon" href="' + favicon + '" type="image/' + iconType + '">');
|
||||
}
|
||||
|
||||
head.push('<link rel="canonical" href="' + escapeExpression(meta.canonicalUrl) + '">');
|
||||
|
||||
if (_.includes(context, 'preview')) {
|
||||
head.push(writeMetaTag('robots', 'noindex,nofollow', 'name'));
|
||||
head.push(writeMetaTag('referrer', 'same-origin', 'name'));
|
||||
} else {
|
||||
head.push(writeMetaTag('referrer', referrerPolicy, 'name'));
|
||||
}
|
||||
}
|
||||
|
||||
// no output in head if a publication icon is not set
|
||||
if (settingsCache.get('icon')) {
|
||||
head.push('<link rel="icon" href="' + favicon + '" type="image/' + iconType + '">');
|
||||
}
|
||||
|
||||
head.push('<link rel="canonical" href="' + escapeExpression(meta.canonicalUrl) + '">');
|
||||
|
||||
if (_.includes(context, 'preview')) {
|
||||
head.push(writeMetaTag('robots', 'noindex,nofollow', 'name'));
|
||||
head.push(writeMetaTag('referrer', 'same-origin', 'name'));
|
||||
} else {
|
||||
head.push(writeMetaTag('referrer', referrerPolicy, 'name'));
|
||||
}
|
||||
|
||||
// show amp link in post when 1. we are not on the amp page and 2. amp is enabled
|
||||
if (_.includes(context, 'post') && !_.includes(context, 'amp') && settingsCache.get('amp')) {
|
||||
head.push('<link rel="amphtml" href="' +
|
||||
|
@ -270,45 +273,51 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
|||
}
|
||||
|
||||
if (!_.includes(context, 'paged') && useStructuredData) {
|
||||
head.push('');
|
||||
head.push.apply(head, finaliseStructuredData(meta));
|
||||
head.push('');
|
||||
|
||||
if (meta.schema) {
|
||||
if (!excludeList.has('social_data')) {
|
||||
head.push('');
|
||||
head.push.apply(head, finaliseStructuredData(meta));
|
||||
head.push('');
|
||||
}
|
||||
|
||||
if (!excludeList.has('schema') && meta.schema) {
|
||||
head.push('<script type="application/ld+json">\n' +
|
||||
JSON.stringify(meta.schema, null, ' ') +
|
||||
'\n </script>\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
head.push('<meta name="generator" content="Ghost ' +
|
||||
escapeExpression(safeVersion) + '">');
|
||||
|
||||
head.push('<link rel="alternate" type="application/rss+xml" title="' +
|
||||
escapeExpression(meta.site.title) + '" href="' +
|
||||
escapeExpression(meta.rssUrl) + '">');
|
||||
|
||||
// no code injection for amp context!!!
|
||||
if (!_.includes(context, 'amp')) {
|
||||
head.push(getMembersHelper(options.data, frontendKey));
|
||||
head.push(getSearchHelper(frontendKey));
|
||||
head.push(getAnnouncementBarHelper(options.data));
|
||||
head.push(getMembersHelper(options.data, frontendKey, excludeList)); // controlling for excludes within the function
|
||||
if (!excludeList.has('search')) {
|
||||
head.push(getSearchHelper(frontendKey));
|
||||
}
|
||||
if (!excludeList.has('announcement')) {
|
||||
head.push(getAnnouncementBarHelper(options.data));
|
||||
}
|
||||
try {
|
||||
head.push(getWebmentionDiscoveryLink());
|
||||
} catch (err) {
|
||||
logging.warn(err);
|
||||
}
|
||||
|
||||
|
||||
// @TODO do this in a more "frameworky" way
|
||||
if (cardAssets.hasFile('js')) {
|
||||
head.push(`<script defer src="${getAssetUrl('public/cards.min.js')}"></script>`);
|
||||
}
|
||||
if (cardAssets.hasFile('css')) {
|
||||
head.push(`<link rel="stylesheet" type="text/css" href="${getAssetUrl('public/cards.min.css')}">`);
|
||||
|
||||
if (!excludeList.has('card_assets')) {
|
||||
if (cardAssets.hasFile('js')) {
|
||||
head.push(`<script defer src="${getAssetUrl('public/cards.min.js')}"></script>`);
|
||||
}
|
||||
if (cardAssets.hasFile('css')) {
|
||||
head.push(`<link rel="stylesheet" type="text/css" href="${getAssetUrl('public/cards.min.css')}">`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsCache.get('comments_enabled') !== 'off') {
|
||||
if (!excludeList.has('comment_counts') && settingsCache.get('comments_enabled') !== 'off') {
|
||||
head.push(`<script defer src="${getAssetUrl('public/comment-counts.min.js')}" data-ghost-comments-counts-api="${urlUtils.getSiteUrl(true)}members/api/comments/counts/"></script>`);
|
||||
}
|
||||
|
||||
|
@ -327,7 +336,6 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
|||
head.push(styleTag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(globalCodeinjection)) {
|
||||
head.push(globalCodeinjection);
|
||||
}
|
||||
|
@ -339,7 +347,7 @@ 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));
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@ const models = require('../../../../core/server/models');
|
|||
const imageLib = require('../../../../core/server/lib/image');
|
||||
const routing = require('../../../../core/frontend/services/routing');
|
||||
const urlService = require('../../../../core/server/services/url');
|
||||
const {cardAssets} = require('../../../../core/frontend/services/assets-minification');
|
||||
const logging = require('@tryghost/logging');
|
||||
|
||||
const ghost_head = require('../../../../core/frontend/helpers/ghost_head');
|
||||
|
@ -1466,4 +1467,187 @@ describe('{{ghost_head}} helper', function () {
|
|||
}));
|
||||
});
|
||||
});
|
||||
describe('respects values from excludes: ', function () {
|
||||
it('when excludes is empty', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: ''}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.match(/portal@/);
|
||||
rendered.should.match(/sodo-search@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
});
|
||||
it('when exclude contains search', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: 'search'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/sodo-search@/);
|
||||
rendered.should.match(/portal@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
});
|
||||
it('when exclude contains portal', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: 'portal'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.match(/sodo-search@/);
|
||||
rendered.should.not.match(/portal@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
});
|
||||
it('can handle multiple excludes', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: 'portal,search'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/sodo-search@/);
|
||||
rendered.should.not.match(/portal@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
});
|
||||
|
||||
it('shows the announcement when exclude does not contain announcement', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('announcement_content').returns('Hello world');
|
||||
settingsCache.get.withArgs('announcement_visibility').returns('visitors');
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: ''}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.match(/sodo-search@/);
|
||||
rendered.should.match(/portal@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
rendered.should.match(/announcement-bar@/);
|
||||
});
|
||||
it('does not show the announcement when exclude contains announcement', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('announcement_content').returns('Hello world');
|
||||
settingsCache.get.withArgs('announcement_visibility').returns('visitors');
|
||||
|
||||
let rendered = await testGhostHead({hash: {exclude: 'announcement'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '4.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.match(/sodo-search@/);
|
||||
rendered.should.match(/portal@/);
|
||||
rendered.should.match(/js.stripe.com/);
|
||||
rendered.should.match(/generator/);
|
||||
rendered.should.not.match(/announcement-bar@/);
|
||||
});
|
||||
|
||||
it('does not load the comments script when exclude contains comment_counts', async function () {
|
||||
settingsCache.get.withArgs('comments_enabled').returns('all');
|
||||
let rendered = await testGhostHead({hash: {exclude: 'comment_counts'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/comment-counts.min.js/);
|
||||
});
|
||||
|
||||
it('loads card assets when not excluded', async function () {
|
||||
// mock the card assets cardAssets.hasFile('js', 'cards.min.js').returns(true);
|
||||
sinon.stub(cardAssets, 'hasFile').returns(true);
|
||||
|
||||
let rendered = await testGhostHead({...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.match(/cards.min.js/);
|
||||
rendered.should.match(/cards.min.css/);
|
||||
});
|
||||
it('does not load card assets when excluded with card_assets', async function () {
|
||||
sinon.stub(cardAssets, 'hasFile').returns(true);
|
||||
let rendered = await testGhostHead({hash: {exclude: 'card_assets'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/cards.min.js/);
|
||||
rendered.should.not.match(/cards.min.css/);
|
||||
});
|
||||
it('does not load meta tags when excluded with metadata', async function () {
|
||||
let rendered = await testGhostHead({hash: {exclude: 'metadata'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/<link rel="canonical"/);
|
||||
});
|
||||
it('does not load schema when excluded with schema', async function () {
|
||||
let rendered = await testGhostHead({hash: {exclude: 'schema'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/<script type="application\/ld\+json"/);
|
||||
});
|
||||
it('does not load og: or twitter: attributes when excludd with social_data', async function () {
|
||||
let rendered = await testGhostHead({hash: {exclude: 'social_data'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/<meta property="og:/);
|
||||
rendered.should.not.match(/<meta property="twitter:/);
|
||||
});
|
||||
it('does not load cta styles when excluded with cta_styles', async function () {
|
||||
settingsCache.get.withArgs('members_enabled').returns(true);
|
||||
settingsCache.get.withArgs('paid_members_enabled').returns(true);
|
||||
let rendered = await testGhostHead({hash: {exclude: 'cta_styles'}, ...testUtils.createHbsResponse({
|
||||
locals: {
|
||||
relativeUrl: '/',
|
||||
context: ['home', 'index'],
|
||||
safeVersion: '0.3'
|
||||
}
|
||||
})});
|
||||
rendered.should.not.match(/.gh-post-upgrade-cta-content/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue