diff --git a/ghost/core/core/frontend/helpers/ghost_head.js b/ghost/core/core/frontend/helpers/ghost_head.js
index ccf64edd2d..16745877a7 100644
--- a/ghost/core/core/frontend/helpers/ghost_head.js
+++ b/ghost/core/core/frontend/helpers/ghost_head.js
@@ -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 += ``;
+ }
+ if (!excludeList.has('cta_styles')) {
+ membersHelper += (``);
}
- const dataAttributes = getDataAttributes(attributes);
-
- let membersHelper = ``;
- membersHelper += (``);
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('');
+ if (!excludeList.has('metadata')) {
+ // head is our main array that holds our meta data
+ if (meta.metaDescription && meta.metaDescription.length > 0) {
+ head.push('');
+ }
+
+ // no output in head if a publication icon is not set
+ if (settingsCache.get('icon')) {
+ head.push('');
+ }
+
+ head.push('');
+
+ 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('');
- }
-
- head.push('');
-
- 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('\n' +
JSON.stringify(meta.schema, null, ' ') +
'\n \n');
}
}
}
-
head.push('');
-
head.push('');
-
// 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(``);
- }
- if (cardAssets.hasFile('css')) {
- head.push(``);
+
+ if (!excludeList.has('card_assets')) {
+ if (cardAssets.hasFile('js')) {
+ head.push(``);
+ }
+ if (cardAssets.hasFile('css')) {
+ head.push(``);
+ }
}
- if (settingsCache.get('comments_enabled') !== 'off') {
+ if (!excludeList.has('comment_counts') && settingsCache.get('comments_enabled') !== 'off') {
head.push(``);
}
@@ -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));
}
diff --git a/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap b/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap
index b3fc1e298c..3a4938eeee 100644
--- a/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap
+++ b/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap
@@ -2428,6 +2428,1012 @@ Object {
}
`;
+exports[`{{ghost_head}} helper respects values from excludes: can handle multiple excludes 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load card assets when excluded with card_assets 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load cta styles when excluded with cta_styles 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load meta tags when excluded with metadata 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load og: or twitter: attributes when excludd with social_data 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load schema when excluded with schema 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not load the comments script when exclude contains comment_counts 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: does not show the announcement when exclude contains announcement 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: loads card assets when not excluded 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: shows the announcement when exclude does not contain announcement 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: when exclude contains portal 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: when exclude contains search 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
+exports[`{{ghost_head}} helper respects values from excludes: when excludes is empty 1 1`] = `
+Object {
+ "rendered": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ",
+}
+`;
+
exports[`{{ghost_head}} helper search scripts includes search when labs flag enabled 1 1`] = `
Object {
"rendered": "
diff --git a/ghost/core/test/unit/frontend/helpers/ghost_head.test.js b/ghost/core/test/unit/frontend/helpers/ghost_head.test.js
index fb88e0d52d..1adf73f2d1 100644
--- a/ghost/core/test/unit/frontend/helpers/ghost_head.test.js
+++ b/ghost/core/test/unit/frontend/helpers/ghost_head.test.js
@@ -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(/