Skip to content

Commit

Permalink
✨ Added "exclude" option for customizing {{ghost_head}} (TryGhost#21229)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cathysarisky authored Oct 31, 2024
1 parent 7e50a40 commit f601ab3
Show file tree
Hide file tree
Showing 3 changed files with 1,252 additions and 54 deletions.
116 changes: 62 additions & 54 deletions ghost/core/core/frontend/helpers/ghost_head.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}

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;
let membersHelper = '';
if (!excludeList.has('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 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' : '';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 + '">');
}
// 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) + '">');
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'));
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="' +
Expand All @@ -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>`);
}

Expand All @@ -327,7 +336,6 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
head.push(styleTag);
}
}

if (!_.isEmpty(globalCodeinjection)) {
head.push(globalCodeinjection);
}
Expand All @@ -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));
}
Expand Down
Loading

0 comments on commit f601ab3

Please sign in to comment.