Ghost/core/server/helpers/ghost_head.js
Hannah Wolfe 8d1e729f30 RSS Refactor with cache
refs #5091, refs #2263

- Move rss handling out of the frontend controller and into its own module
- Separate the code into logical blocks
- Wrap the generation code in a in-memory cache to prevent it being regenerated on every request
2015-04-10 21:32:14 +01:00

327 lines
11 KiB
JavaScript

// # Ghost Head Helper
// Usage: `{{ghost_head}}`
//
// Outputs scripts and other assets at the top of a Ghost theme
//
// We use the name ghost_head to match the helper for consistency:
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var hbs = require('express-hbs'),
moment = require('moment'),
_ = require('lodash'),
Promise = require('bluebird'),
config = require('../config'),
filters = require('../filters'),
api = require('../api'),
urlHelper = require('./url'),
meta_description = require('./meta_description'),
meta_title = require('./meta_title'),
excerpt = require('./excerpt'),
tagsHelper = require('./tags'),
imageHelper = require('./image'),
blog,
ghost_head;
function getImage(ops, context, contextObject) {
if (context === 'home' || context === 'author') {
contextObject.image = contextObject.cover;
}
ops.push(imageHelper.call(contextObject, {hash: {absolute: true}}));
if (context === 'post' && contextObject.author) {
ops.push(imageHelper.call(contextObject.author, {hash: {absolute: true}}));
}
return ops;
}
function getPaginationUrls(pagination, relativeUrl, secure, head) {
var trimmedUrl, next, prev,
trimmedUrlpattern = /.+(?=\/page\/\d*\/)/,
tagOrAuthorPattern = /\/(tag)|(author)\//;
trimmedUrl = relativeUrl.match(trimmedUrlpattern);
if (pagination.prev) {
prev = (pagination.prev > 1 ? '/page/' + pagination.prev + '/' : '/');
prev = (trimmedUrl) ? trimmedUrl + prev : prev;
head.push('<link rel="prev" href="' +
config.urlFor({relativeUrl: prev, secure: secure}, true) + '" />'
);
}
if (pagination.next) {
next = '/page/' + pagination.next + '/';
if (trimmedUrl) {
next = trimmedUrl + next;
} else if (tagOrAuthorPattern.test(relativeUrl)) {
next = relativeUrl.slice(0, -1) + next;
}
head.push('<link rel="next" href="' +
config.urlFor({relativeUrl: next, secure: secure}, true) + '" />'
);
}
return head;
}
function addContextMetaData(context, data, metaData) {
// escaped data
metaData.metaTitle = hbs.handlebars.Utils.escapeExpression(metaData.metaTitle);
metaData.metaDescription = metaData.metaDescription ? hbs.handlebars.Utils.escapeExpression(metaData.metaDescription) : null;
if (context === 'author') {
metaData.authorUrl = hbs.handlebars.Utils.escapeExpression(blog.url + '/author/' + data.author.slug);
metaData.ogType = 'profile';
} else if (context === 'post') {
metaData.publishedDate = moment(data.post.published_at).toISOString();
metaData.modifiedDate = moment(data.post.updated_at).toISOString();
metaData.authorUrl = hbs.handlebars.Utils.escapeExpression(blog.url + '/author/' + data.post.author.slug);
metaData.ogType = 'article';
}
return metaData;
}
function initMetaData(context, data, results) {
var metaData = {
url: results[0].value(),
metaDescription: results[1].value() || null,
metaTitle: results[2].value(),
coverImage: results.length > 3 ? results[3].value() : null,
authorImage: results.length > 4 ? results[4].value() : null,
publishedDate: null,
modifiedDate: null,
tags: null,
card: 'summary',
authorUrl: null,
ogType: 'website',
keywords: null,
blog: blog,
title: blog.title
};
if (!metaData.metaDescription) {
if (context === 'post') {
metaData.metaDescription = excerpt.call(data.post, {hash: {words: '40'}}).string + '...';
} else if (context === 'tag') {
metaData.metaDescription = data.tag.description ? data.tag.description : null;
}
}
return addContextMetaData(context, data, metaData);
}
function getStructuredData(metaData) {
var structuredData;
if (metaData.coverImage) {
metaData.card = 'summary_large_image';
}
structuredData = {
'og:site_name': metaData.title,
'og:type': metaData.ogType,
'og:title': metaData.metaTitle,
'og:description': metaData.metaDescription,
'og:url': metaData.url,
'og:image': metaData.coverImage,
'article:published_time': metaData.publishedDate,
'article:modified_time': metaData.modifiedDate,
'article:tag': metaData.tags,
'twitter:card': metaData.card,
'twitter:title': metaData.metaTitle,
'twitter:description': metaData.metaDescription,
'twitter:url': metaData.url,
'twitter:image:src': metaData.coverImage
};
return structuredData;
}
// Creates the final schema object with values that are not null
function trimSchema(schema) {
var schemaObject = {};
_.each(schema, function (value, key) {
if (value !== null && value !== undefined) {
schemaObject[key] = value;
}
});
return schemaObject;
}
function getPostSchema(metaData, data) {
var schema = {
'@context': 'http://schema.org',
'@type': 'Article',
publisher: metaData.title,
author: {
'@type': 'Person',
name: data.post.author.name,
image: metaData.authorImage,
url: metaData.authorUrl,
sameAs: data.post.author.website || null,
description: data.post.author.bio || null
},
headline: metaData.metaTitle,
url: metaData.url,
datePublished: metaData.publishedDate,
dateModified: metaData.modifiedDate,
image: metaData.coverImage,
keywords: metaData.keywords,
description: metaData.metaDescription
};
return trimSchema(schema);
}
function getTagSchema(metaData, data) {
var schema = {
'@context': 'http://schema.org',
'@type': 'Series',
publisher: metaData.title,
url: metaData.url,
image: metaData.coverImage,
name: data.tag.name,
description: metaData.metaDescription
};
return trimSchema(schema);
}
function getAuthorSchema(metaData, data) {
var schema = {
'@context': 'http://schema.org',
'@type': 'Person',
sameAs: data.author.website || null,
publisher: metaData.title,
name: data.author.name,
url: metaData.authorUrl,
image: metaData.coverImage,
description: metaData.metaDescription
};
return trimSchema(schema);
}
function getHomeSchema(metaData) {
var schema = {
'@context': 'http://schema.org',
'@type': 'Website',
publisher: metaData.title,
url: metaData.url,
image: metaData.coverImage,
description: metaData.metaDescription
};
return trimSchema(schema);
}
function chooseSchema(metaData, context, data) {
if (context === 'post') {
return getPostSchema(metaData, data);
} else if (context === 'home') {
return getHomeSchema(metaData);
} else if (context === 'tag') {
return getTagSchema(metaData, data);
} else if (context === 'author') {
return getAuthorSchema(metaData, data);
}
}
function finaliseStructuredData(structuredData, tags, head) {
var type;
_.each(structuredData, function (content, property) {
if (property === 'article:tag') {
_.each(tags, function (tag) {
if (tag !== '') {
tag = hbs.handlebars.Utils.escapeExpression(tag.trim());
head.push('<meta property="' + property + '" content="' + tag + '" />');
}
});
head.push('');
} else if (content !== null && content !== undefined) {
type = property.substring(0, 7) === 'twitter' ? 'name' : 'property';
head.push('<meta ' + type + '="' + property + '" content="' + content + '" />');
}
});
return head;
}
function finaliseSchema(schema, head) {
head.push('<script type="application/ld+json">\n' + JSON.stringify(schema, null, ' ') +
'\n </script>\n'
);
return head;
}
ghost_head = function (options) {
// create a shortcut for theme config
blog = config.theme;
/*jshint unused:false*/
var self = this,
useStructuredData = !config.isPrivacyDisabled('useStructuredData'),
head = [],
safeVersion = this.safeVersion,
ops = [],
structuredData,
schema,
title = hbs.handlebars.Utils.escapeExpression(blog.title),
context = self.context[0],
contextObject = self[context] || blog;
// Push Async calls to an array of promises
ops.push(urlHelper.call(self, {hash: {absolute: true}}));
ops.push(meta_description.call(self, options));
ops.push(meta_title.call(self, options));
ops = getImage(ops, context, contextObject);
// Resolves promises then push pushes meta data into ghost_head
return Promise.settle(ops).then(function (results) {
var metaData = initMetaData(context, self, results),
tags = tagsHelper.call(self.post, {hash: {autolink: 'false'}}).string.split(',');
// If there are tags - build the keywords metaData string
if (tags[0] !== '') {
metaData.keywords = hbs.handlebars.Utils.escapeExpression(tagsHelper.call(self.post,
{hash: {autolink: 'false', separator: ', '}}
).string);
}
// head is our main array that holds our meta data
head.push('<link rel="canonical" href="' + metaData.url + '" />');
// Generate context driven pagination urls
if (self.pagination) {
getPaginationUrls(self.pagination, self.relativeUrl, self.secure, head);
}
// Test to see if we are on a post page and that Structured data has not been disabled in config.js
if (context !== 'paged' && context !== 'page' && useStructuredData) {
// Create context driven OpenGraph and Twitter meta data
structuredData = getStructuredData(metaData);
// Create context driven JSONLD object
schema = chooseSchema(metaData, context, self);
head.push('');
// Formats structured data and pushes to head array
finaliseStructuredData(structuredData, tags, head);
head.push('');
// Formats schema script/JSONLD data and pushes to head array
finaliseSchema(schema, head);
}
head.push('<meta name="generator" content="Ghost ' + safeVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="' +
title + '" href="' + config.urlFor('rss', null, true) + '" />');
}).then(function () {
return api.settings.read({key: 'ghost_head'});
}).then(function (response) {
head.push(response.settings[0].value);
return filters.doFilter('ghost_head', head);
}).then(function (head) {
var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, '');
return new hbs.handlebars.SafeString(headString.trim());
});
};
module.exports = ghost_head;