2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Merge pull request #4891 from cobbspur/structured2

Adds structured data to first index/tag/author page
This commit is contained in:
Hannah Wolfe 2015-04-07 21:24:19 +01:00
commit 4839b58ff0
2 changed files with 809 additions and 324 deletions

View file

@ -21,143 +21,295 @@ var hbs = require('express-hbs'),
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,
blog = config.theme,
useStructuredData = !config.isPrivacyDisabled('useStructuredData'),
head = [],
majorMinor = /^(\d+\.)?(\d+)/,
trimmedVersion = this.version,
trimmedUrlpattern = /.+(?=\/page\/\d*\/)/,
tagOrAuthorPattern = /\/(tag)|(author)\//,
trimmedUrl, next, prev, tags,
ops = [],
structuredData,
coverImage, authorImage, keywords,
schema,
title = hbs.handlebars.Utils.escapeExpression(blog.title);
title = hbs.handlebars.Utils.escapeExpression(blog.title),
context = self.context[0],
contextObject = self[context] || blog;
trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?';
// 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));
if (self.post) {
ops.push(imageHelper.call(self.post, {hash: {absolute:true}}));
if (self.post.author) {
ops.push(imageHelper.call(self.post.author, {hash: {absolute:true}}));
}
}
ops = getImage(ops, context, contextObject);
// Resolves promises then push pushes meta data into ghost_head
return Promise.settle(ops).then(function (results) {
var url = results[0].value(),
metaDescription = results[1].value(),
metaTitle = results[2].value(),
coverImage = results.length > 3 ? results[3].value() : null,
authorImage = results.length > 4 ? results[4].value() : null,
publishedDate, modifiedDate,
tags = tagsHelper.call(self.post, {hash: {autolink: 'false'}}).string.split(','),
card = 'summary',
type, authorUrl;
var metaData = initMetaData(context, self, results),
tags = tagsHelper.call(self.post, {hash: {autolink: 'false'}}).string.split(',');
if (!metaDescription) {
metaDescription = excerpt.call(self.post, {hash: {words: '40'}}).string;
}
// If there are tags - build the keywords metaData string
if (tags[0] !== '') {
keywords = hbs.handlebars.Utils.escapeExpression(tagsHelper.call(self.post, {hash: {autolink: 'false', separator: ', '}}).string);
metaData.keywords = hbs.handlebars.Utils.escapeExpression(tagsHelper.call(self.post,
{hash: {autolink: 'false', separator: ', '}}
).string);
}
head.push('<link rel="canonical" href="' + url + '" />');
// 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) {
trimmedUrl = self.relativeUrl.match(trimmedUrlpattern);
if (self.pagination.prev) {
prev = (self.pagination.prev > 1 ? prev = '/page/' + self.pagination.prev + '/' : prev = '/');
prev = (trimmedUrl) ? trimmedUrl + prev : prev;
head.push('<link rel="prev" href="' + config.urlFor({relativeUrl: prev, secure: self.secure}, true) + '" />');
}
if (self.pagination.next) {
next = '/page/' + self.pagination.next + '/';
if (trimmedUrl) {
next = trimmedUrl + next;
} else if (tagOrAuthorPattern.test(self.relativeUrl)) {
next = self.relativeUrl.slice(0, -1) + next;
}
head.push('<link rel="next" href="' + config.urlFor({relativeUrl: next, secure: self.secure}, true) + '" />');
}
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 (self.post && useStructuredData) {
publishedDate = moment(self.post.published_at).toISOString();
modifiedDate = moment(self.post.updated_at).toISOString();
if (coverImage) {
card = 'summary_large_image';
}
// escaped data
metaTitle = hbs.handlebars.Utils.escapeExpression(metaTitle);
metaDescription = hbs.handlebars.Utils.escapeExpression(metaDescription + '...');
authorUrl = hbs.handlebars.Utils.escapeExpression(blog.url + '/author/' + self.post.author.slug);
schema = {
'@context': 'http://schema.org',
'@type': 'Article',
publisher: title,
author: {
'@type': 'Person',
name: self.post.author.name,
image: authorImage,
url: authorUrl,
sameAs: self.post.author.website
},
headline: metaTitle,
url: url,
datePublished: publishedDate,
dateModified: modifiedDate,
image: coverImage,
keywords: keywords,
description: metaDescription
};
structuredData = {
'og:site_name': title,
'og:type': 'article',
'og:title': metaTitle,
'og:description': metaDescription,
'og:url': url,
'og:image': coverImage,
'article:published_time': publishedDate,
'article:modified_time': modifiedDate,
'article:tag': tags,
'twitter:card': card,
'twitter:title': metaTitle,
'twitter:description': metaDescription,
'twitter:url': url,
'twitter:image:src': coverImage
};
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('');
_.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 + '" />');
}
});
// Formats structured data and pushes to head array
finaliseStructuredData(structuredData, tags, head);
head.push('');
head.push('<script type="application/ld+json">\n' + JSON.stringify(schema, null, ' ') + '\n </script>\n');
// Formats schema script/JSONLD data and pushes to head array
finaliseSchema(schema, head);
}
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');

View file

@ -14,22 +14,26 @@ var should = require('should'),
describe('{{ghost_head}} helper', function () {
var sandbox;
before(function () {
utils.loadHelpers();
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost'
}
});
});
after(function () {
utils.restoreConfig();
});
describe('without Code Injection', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost',
description: 'blog description',
cover: '/content/images/blog-cover.png'
}
});
});
after(function () {
utils.restoreConfig();
});
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
@ -49,15 +53,241 @@ describe('{{ghost_head}} helper', function () {
should.exist(handlebars.helpers.ghost_head);
});
it('returns meta tag string', function (done) {
it('returns meta tag string on paginated index page without structured data and schema', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', post: false},
{data: {root: {context: []}}}
{version: '0.3.0', relativeUrl: '/page/2/', context: ['paged', 'index']},
{data: {root: {context: ['paged', 'index']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/page\/2\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
it('returns structured data on first index page', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/', context: ['home', 'index']},
{data: {root: {context: ['home', 'index']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="website" \/>/);
rendered.string.should.match(/<meta property="og:title" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:description" content="blog description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/blog-cover.png" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="blog description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/blog-cover.png" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Website"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/"/);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/blog-cover.png"/);
rendered.string.should.match(/"description": "blog description"/);
done();
}).catch(done);
});
it('returns structured data and schema first tag page with meta description and meta title', function (done) {
var tag = {
meta_description: 'tag meta description',
name: 'tagtitle',
meta_title: 'tag meta title',
image: '/content/images/tag-image.png'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']},
{data: {root: {context: ['tag']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="website" \/>/);
rendered.string.should.match(/<meta property="og:title" content="tag meta title" \/>/);
rendered.string.should.match(/<meta property="og:description" content="tag meta description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/tag-image.png" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="tag meta title" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="tag meta description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/tag-image.png" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Series"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/tag\/tagtitle\/"/);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/tag-image.png"/);
rendered.string.should.match(/"name": "tagtitle"/);
rendered.string.should.match(/"description": "tag meta description"/);
done();
}).catch(done);
});
it('tag first page without meta description uses tag description, and title if no meta title', function (done) {
var tag = {
meta_description: '',
description: 'tag description',
name: 'tagtitle',
meta_title: '',
image: '/content/images/tag-image.png'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']},
{data: {root: {context: ['tag']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="website" \/>/);
rendered.string.should.match(/<meta property="og:title" content="tagtitle - Ghost" \/>/);
rendered.string.should.match(/<meta property="og:description" content="tag description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/tag-image.png" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="tagtitle - Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="tag description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/tag\/tagtitle\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/tag-image.png" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Series"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/tag\/tagtitle\/"/);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/tag-image.png"/);
rendered.string.should.match(/"name": "tagtitle"/);
rendered.string.should.match(/"description": "tag description"/);
done();
}).catch(done);
});
it('tag first page with meta and model description returns no description fields', function (done) {
var tag = {
meta_description: '',
name: 'tagtitle',
meta_title: '',
image: '/content/images/tag-image.png'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']},
{data: {root: {context: ['tag']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.not.match(/<meta property="og:description" \/>/);
rendered.string.should.not.match(/<meta name="twitter:description"\/>/);
rendered.string.should.not.match(/"description":/);
done();
}).catch(done);
});
it('does not return structured data on paginated tag pages', function (done) {
var tag = {
meta_description: 'tag meta description',
title: 'tagtitle',
meta_title: 'tag meta title',
image: '/content/images/tag-image.png'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/tag/tagtitle/page/2/', tag: tag, context: ['paged', 'tag']},
{data: {root: {context: ['tag']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/tag\/tagtitle\/page\/2\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
it('returns structured data and schema on first author page with cover image', function (done) {
var author = {
name: 'Author name',
slug: 'AuthorName',
bio: 'Author bio',
image: '/content/images/test-author-image.png',
cover: '/content/images/author-cover-image.png',
website: 'http://authorwebsite.com'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/author/AuthorName/', author: author, context: ['author']},
{data: {root: {context: ['author']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/author\/AuthorName\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="profile" \/>/);
rendered.string.should.match(/<meta property="og:description" content="Author bio" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/author\/AuthorName\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/author-cover-image.png" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Author name - Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="Author bio" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/author\/AuthorName\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/author-cover-image.png" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Person"/);
rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/AuthorName"/);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/author-cover-image.png"/);
rendered.string.should.match(/"name": "Author name"/);
rendered.string.should.match(/"description": "Author bio"/);
done();
}).catch(done);
});
it('does not return structured data on paginated author pages', function (done) {
var author = {
name: 'Author name',
slug: 'AuthorName',
bio: 'Author bio',
image: '/content/images/test-author-image.png',
cover: '/content/images/author-cover-image.png',
website: 'http://authorwebsite.com'
};
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/author/AuthorName/page/2/', author: author, context: ['paged', 'author']},
{data: {root: {context: ['paged', 'author']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/author\/AuthorName\/page\/2\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
@ -65,13 +295,13 @@ describe('{{ghost_head}} helper', function () {
it('returns meta tag string even if version is invalid', function (done) {
helpers.ghost_head.call(
{version: '0.9'},
{version: '0.9', context: []},
{data: {root: {context: []}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.9" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.9" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
done();
}).catch(done);
@ -90,43 +320,60 @@ describe('{{ghost_head}} helper', function () {
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
website: 'http://authorwebsite.com',
bio: 'Author bio'
}
};
helpers.ghost_head.call(
{relativeUrl: '/post/', version: '0.3.0', post: post},
{relativeUrl: '/post/', version: '0.3.0', context: ['post'], post: post},
{data: {root: {context: ['post']}}}
).then(function (rendered) {
var re1 = new RegExp('<meta property="article:published_time" content="' + post.published_at),
re2 = new RegExp('<meta property="article:modified_time" content="' + post.updated_at),
re3 = new RegExp('"datePublished": "' + post.published_at),
re4 = new RegExp('"dateModified": "' + post.updated_at);
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost" />\n' +
' <meta property="og:description" content="blog description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="article" \/>/);
rendered.string.should.match(/<meta property="og:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta property="og:description" content="blog description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(re1);
rendered.string.should.match(re2);
rendered.string.should.match(/<meta property="article:tag" content="tag1" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag2" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag3" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="blog description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Article"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"author": {/);
rendered.string.should.match(/"@type": "Person"/);
rendered.string.should.match(/"name": "Author name"/);
rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/);
rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/);
rendered.string.should.match(/"description": "Author bio"/);
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/);
rendered.string.should.match(re3);
rendered.string.should.match(re4);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/test-image.png"/);
rendered.string.should.match(/"keywords": "tag1, tag2, tag3"/);
rendered.string.should.match(/"description": "blog description"/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
done();
}).catch(done);
@ -151,38 +398,54 @@ describe('{{ghost_head}} helper', function () {
};
helpers.ghost_head.call(
{relativeUrl: '/post/', version: '0.3.0', post: post},
{relativeUrl: '/post/', version: '0.3.0', context: ['post'], post: post},
{data: {root: {context: ['post']}}}
).then(function (rendered) {
var re1 = new RegExp('<meta property="article:published_time" content="' + post.published_at),
re2 = new RegExp('<meta property="article:modified_time" content="' + post.updated_at),
re3 = new RegExp('"datePublished": "' + post.published_at),
re4 = new RegExp('"dateModified": "' + post.updated_at);
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta property="og:description" content="blog &quot;test&quot; description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta name="twitter:description" content="blog &quot;test&quot; description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost &quot;test&quot;",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog &quot;test&quot; description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="article" \/>/);
rendered.string.should.match(/<meta property="og:title" content="Welcome to Ghost &quot;test&quot;" \/>/);
rendered.string.should.match(/<meta property="og:description" content="blog &quot;test&quot; description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(re1);
rendered.string.should.match(re2);
rendered.string.should.match(/<meta property="article:tag" content="tag1" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag2" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag3" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost &quot;test&quot;" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="blog &quot;test&quot; description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Article"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"author": {/);
rendered.string.should.match(/"@type": "Person"/);
rendered.string.should.match(/"name": "Author name"/);
rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/);
rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/);
rendered.string.should.match(/"headline": "Welcome to Ghost &quot;test&quot;"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(re3);
rendered.string.should.match(re4);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/test-image.png"/);
rendered.string.should.match(/"keywords": "tag1, tag2, tag3"/);
rendered.string.should.match(/"description": "blog &quot;test&quot; description"/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
done();
}).catch(done);
@ -206,35 +469,51 @@ describe('{{ghost_head}} helper', function () {
};
helpers.ghost_head.call(
{relativeUrl: '/post/', version: '0.3.0', post: post},
{data: {root: {context: ['post']}}}
).then(function (rendered) {
{relativeUrl: '/post/', version: '0.3.0', context: ['post'], post: post},
{data: {root: {context: ['post']}}}).then(function (rendered) {
var re1 = new RegExp('<meta property="article:published_time" content="' + post.published_at),
re2 = new RegExp('<meta property="article:modified_time" content="' + post.updated_at),
re3 = new RegExp('"datePublished": "' + post.published_at),
re4 = new RegExp('"dateModified": "' + post.updated_at);
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost" />\n' +
' <meta property="og:description" content="blog description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="article" \/>/);
rendered.string.should.match(/<meta property="og:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta property="og:description" content="blog description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:image" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(re1);
rendered.string.should.match(re2);
rendered.string.should.not.match(/<meta property="article:tag"/);
rendered.string.should.match(/<meta name="twitter:card" content="summary_large_image" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="blog description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta name="twitter:image:src" content="http:\/\/testurl.com\/content\/images\/test-image.png" \/>/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Article"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"author": {/);
rendered.string.should.match(/"@type": "Person"/);
rendered.string.should.match(/"name": "Author name"/);
rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/);
rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/);
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(re3);
rendered.string.should.match(re4);
rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/test-image.png"/);
rendered.string.should.not.match(/"keywords":/);
rendered.string.should.match(/"description": "blog description"/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
done();
}).catch(done);
@ -258,46 +537,169 @@ describe('{{ghost_head}} helper', function () {
};
helpers.ghost_head.call(
{relativeUrl: '/post/', version: '0.3.0', post: post},
{relativeUrl: '/post/', version: '0.3.0', context: ['post'], post: post},
{data: {root: {context: ['post']}}}
).then(function (rendered) {
var re1 = new RegExp('<meta property="article:published_time" content="' + post.published_at),
re2 = new RegExp('<meta property="article:modified_time" content="' + post.updated_at),
re3 = new RegExp('"datePublished": "' + post.published_at),
re4 = new RegExp('"dateModified": "' + post.updated_at);
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost" />\n' +
' <meta property="og:description" content="blog description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "keywords": "tag1, tag2, tag3",\n "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta property="og:site_name" content="Ghost" \/>/);
rendered.string.should.match(/<meta property="og:type" content="article" \/>/);
rendered.string.should.match(/<meta property="og:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta property="og:description" content="blog description" \/>/);
rendered.string.should.match(/<meta property="og:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.not.match(/<meta property="og:image"/);
rendered.string.should.match(re1);
rendered.string.should.match(re2);
rendered.string.should.match(/<meta property="article:tag" content="tag1" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag2" \/>/);
rendered.string.should.match(/<meta property="article:tag" content="tag3" \/>/);
rendered.string.should.match(/<meta name="twitter:card" content="summary" \/>/);
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost" \/>/);
rendered.string.should.match(/<meta name="twitter:description" content="blog description" \/>/);
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.not.match(/<meta name="twitter:image:src"/);
rendered.string.should.match(/<script type=\"application\/ld\+json\">/);
rendered.string.should.match(/"@context": "http:\/\/schema.org"/);
rendered.string.should.match(/"@type": "Article"/);
rendered.string.should.match(/"publisher": "Ghost"/);
rendered.string.should.match(/"author": {/);
rendered.string.should.match(/"@type": "Person"/);
rendered.string.should.match(/"name": "Author name"/);
rendered.string.should.not.match(/"image\"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/);
rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/);
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/);
rendered.string.should.match(re3);
rendered.string.should.match(re4);
rendered.string.should.match(/"keywords": "tag1, tag2, tag3"/);
rendered.string.should.match(/"description": "blog description"/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
done();
}).catch(done);
});
it('does not return structured data if useStructuredData is set to false in config file', function (done) {
it('returns canonical URL', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/about/', context: ['page']},
{data: {root: {context: ['page']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/about\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
it('returns next & prev URL correctly for middle page', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/page/3/', context: ['paged', 'index'], pagination: {next: '4', prev: '2'}},
{data: {root: {context: ['index', 'paged'], pagination: {total: 4, page: 3, next: 4, prev: 2}}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/page\/3\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="prev" href="http:\/\/testurl.com\/page\/2\/" \/>/);
rendered.string.should.match(/<link rel="next" href="http:\/\/testurl.com\/page\/4\/" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
it('returns next & prev URL correctly for second page', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/page/2/', context: ['paged', 'index'], pagination: {next: '3', prev: '1'}},
{data: {root: {context: ['index', 'paged'], pagination: {total: 3, page: 2, next: 3, prev: 1}}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/page\/2\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="prev" href="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<link rel="next" href="http:\/\/testurl.com\/page\/3\/" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
describe('with /blog subdirectory', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/blog/',
theme: {
title: 'Ghost',
description: 'blog description',
cover: '/content/images/blog-cover.png'
}
});
});
after(function () {
utils.restoreConfig();
});
it('returns correct rss url with subdirectory', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', context: ['paged', 'index']},
{data: {root: {context: []}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/blog\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/blog\/rss\/" \/>/);
done();
}).catch(done);
});
});
});
describe('with useStructuredData is set to false in config file', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost',
description: 'blog description',
cover: '/content/images/blog-cover.png'
},
privacy: {
useStructuredData: false
}
});
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({
settings: [
{value: ''}
]
});
});
});
after(function () {
utils.restoreConfig();
sandbox.restore();
});
it('does not return structured data', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
@ -315,91 +717,19 @@ describe('{{ghost_head}} helper', function () {
};
helpers.ghost_head.call(
{relativeUrl: '/post/', version: '0.3.0', post: post},
{relativeUrl: '/post/', version: '0.3.0', context: ['post'], post: post},
{data: {root: {context: ['post']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/post\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.not.match(/<meta property="og/);
rendered.string.should.not.match(/<script type=\"application\/ld\+json\">/);
done();
}).catch(done);
});
it('returns canonical URL', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/about/'},
{data: {root: {context: ['page']}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/about/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for middle page', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/page/3/', pagination: {next: '4', prev: '2'}},
{data: {root: {context: ['index', 'paged'], pagination: {total: 4, page: 3, next: 4, prev: 2}}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/page/3/" />\n' +
' <link rel="prev" href="http://testurl.com/page/2/" />\n' +
' <link rel="next" href="http://testurl.com/page/4/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for second page', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', relativeUrl: '/page/2/', pagination: {next: '3', prev: '1'}},
{data: {root: {context: ['index', 'paged'], pagination: {total: 3, page: 2, next: 3, prev: 1}}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/page/2/" />\n' +
' <link rel="prev" href="http://testurl.com/" />\n' +
' <link rel="next" href="http://testurl.com/page/3/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
describe('with /blog subdirectory', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/blog/',
theme: {
title: 'Ghost'
}
});
});
after(function () {
utils.restoreConfig();
});
it('returns correct rss url with subdirectory', function (done) {
helpers.ghost_head.call(
{version: '0.3.0'},
{data: {root: {context: []}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/blog/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" ' +
'href="http://testurl.com/blog/rss/" />');
done();
}).catch(done);
});
});
});
describe('with Code Injection', function () {
@ -410,10 +740,13 @@ describe('{{ghost_head}} helper', function () {
settings: [{value: '<style>body {background: red;}</style>'}]
});
});
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost'
title: 'Ghost',
description: 'blog description',
cover: '/content/images/blog-cover.png'
}
});
});
@ -425,14 +758,14 @@ describe('{{ghost_head}} helper', function () {
it('returns meta tag plus injected code', function (done) {
helpers.ghost_head.call(
{version: '0.3.0', post: false},
{version: '0.3.0', context: ['paged', 'index'], post: false},
{data: {root: {context: []}}}
).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />\n' +
' <style>body {background: red;}</style>');
rendered.string.should.match(/<link rel="canonical" href="http:\/\/testurl.com\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/testurl.com\/rss\/" \/>/);
rendered.string.should.match(/<style>body {background: red;}<\/style>/);
done();
}).catch(done);