mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
✨ Custom Post Excerpt Feature (#8792)
closes #8793 - 1.3 post excerpt migration - add 1.3 migration to add `excerpt` to post schema NOTE: - knex-migrator relies on the package.json safe version - so right now Ghost is on 1.2 - the migration script is for 1.3 - if you pull down the PR (or if we merge this PR into master), you have to run `knex-migrator migrate --v 1.3 --force` - knex-migrator will tell you what you have todo - Bump dependencies - knex-migrator@2.1.3 - Soft limit for custom_excerpt - Extended {{excerpt}} to use custom excerpt - when a `custom_excerpt` field exists, the `{{excerpt}}` helper will output this and fall back to autogenerated excerpt if not. - Refactored behaviour of (meta) description - html tag `<meta name="description" />` for posts, tags and author doesn't get rendered if not provided. - fallback for `author.bio` removed - fallback for `tag.description` removed - structured data and schema.org for `post` context takes the following order to render description fields: 1. custom excerpt 2. meta description 3. automated excerpt (50 words) - updated and added tests to reflect the changes
This commit is contained in:
parent
d8fb7ce7f6
commit
7845617607
17 changed files with 323 additions and 50 deletions
|
@ -6,6 +6,8 @@ function getDescription(data, root) {
|
|||
context = root ? root.context : null,
|
||||
blogDescription = settingsCache.get('description');
|
||||
|
||||
// We only return meta_description if provided. Only exception is the Blog
|
||||
// description, which doesn't rely on meta_description.
|
||||
if (data.meta_description) {
|
||||
description = data.meta_description;
|
||||
} else if (_.includes(context, 'paged')) {
|
||||
|
@ -13,11 +15,14 @@ function getDescription(data, root) {
|
|||
} else if (_.includes(context, 'home')) {
|
||||
description = blogDescription;
|
||||
} else if (_.includes(context, 'author') && data.author) {
|
||||
description = data.author.meta_description || data.author.bio;
|
||||
// The usage of meta data fields for author is currently not implemented.
|
||||
// We do have meta_description and meta_title fields
|
||||
// in the users table, but there's no UI to populate those.
|
||||
description = data.author.meta_description || '';
|
||||
} else if (_.includes(context, 'tag') && data.tag) {
|
||||
description = data.tag.meta_description || data.tag.description;
|
||||
description = data.tag.meta_description || '';
|
||||
} else if ((_.includes(context, 'post') || _.includes(context, 'page')) && data.post) {
|
||||
description = data.post.meta_description;
|
||||
description = data.post.meta_description || '';
|
||||
}
|
||||
|
||||
return (description || '').trim();
|
||||
|
|
|
@ -34,7 +34,7 @@ function getMetaData(data, root) {
|
|||
authorUrl: getAuthorUrl(data, true),
|
||||
rssUrl: getRssUrl(data, true),
|
||||
metaTitle: getTitle(data, root),
|
||||
metaDescription: getDescription(data, root),
|
||||
metaDescription: getDescription(data, root) || null,
|
||||
coverImage: {
|
||||
url: getCoverImage(data, true)
|
||||
},
|
||||
|
@ -67,8 +67,17 @@ function getMetaData(data, root) {
|
|||
metaData.blog.logo = result;
|
||||
|
||||
// TODO: cleanup these if statements
|
||||
if (data.post && data.post.html) {
|
||||
metaData.excerpt = getExcerpt(data.post.html, {words: 50});
|
||||
if (data.post) {
|
||||
// There's a specific order for description fields (not <meta name="description" /> !!) in structured data
|
||||
// and schema.org which is used the description fields (see https://github.com/TryGhost/Ghost/issues/8793):
|
||||
// 1. CASE: custom_excerpt is populated via the UI
|
||||
// 2. CASE: no custom_excerpt, but meta_description is poplated via the UI
|
||||
// 3. CASE: fall back to automated excerpt of 50 words if neither custom_excerpt nor meta_description is provided
|
||||
var customExcerpt = data.post.custom_excerpt,
|
||||
metaDescription = data.post.meta_description,
|
||||
fallbackExcerpt = data.post.html ? getExcerpt(data.post.html, {words: 50}) : '';
|
||||
|
||||
metaData.excerpt = customExcerpt ? customExcerpt : metaDescription ? metaDescription : fallbackExcerpt;
|
||||
}
|
||||
|
||||
if (data.post && data.post.author && data.post.author.name) {
|
||||
|
|
|
@ -64,8 +64,9 @@ function trimSameAs(data, context) {
|
|||
}
|
||||
|
||||
function getPostSchema(metaData, data) {
|
||||
var description = metaData.metaDescription ? escapeExpression(metaData.metaDescription) :
|
||||
(metaData.excerpt ? escapeExpression(metaData.excerpt) : null),
|
||||
// CASE: metaData.excerpt for post context is populated by either the custom excerpt, the meta description,
|
||||
// or the automated excerpt of 50 words. It is empty for any other context.
|
||||
var description = metaData.excerpt ? escapeExpression(metaData.excerpt) : null,
|
||||
schema;
|
||||
|
||||
schema = {
|
||||
|
@ -82,8 +83,8 @@ function getPostSchema(metaData, data) {
|
|||
image: schemaImageObject(metaData.authorImage),
|
||||
url: metaData.authorUrl,
|
||||
sameAs: trimSameAs(data, 'post'),
|
||||
description: data.post.author.bio ?
|
||||
escapeExpression(data.post.author.bio) :
|
||||
description: data.post.author.metaDescription ?
|
||||
escapeExpression(data.post.author.metaDescription) :
|
||||
null
|
||||
},
|
||||
headline: escapeExpression(metaData.metaTitle),
|
||||
|
|
|
@ -12,7 +12,10 @@ function getStructuredData(metaData) {
|
|||
'og:site_name': metaData.blog.title,
|
||||
'og:type': metaData.ogType,
|
||||
'og:title': metaData.metaTitle,
|
||||
'og:description': metaData.metaDescription || metaData.excerpt,
|
||||
// CASE: metaData.excerpt for post context is populated by either the custom excerpt,
|
||||
// the meta description, or the automated excerpt of 50 words. It is empty for any
|
||||
// other context and *always* uses the provided meta description fields.
|
||||
'og:description': metaData.excerpt || metaData.metaDescription,
|
||||
'og:url': metaData.canonicalUrl,
|
||||
'og:image': metaData.coverImage.url,
|
||||
'article:published_time': metaData.publishedDate,
|
||||
|
@ -22,7 +25,7 @@ function getStructuredData(metaData) {
|
|||
'article:author': metaData.authorFacebook ? socialUrls.facebookUrl(metaData.authorFacebook) : undefined,
|
||||
'twitter:card': card,
|
||||
'twitter:title': metaData.metaTitle,
|
||||
'twitter:description': metaData.metaDescription || metaData.excerpt,
|
||||
'twitter:description': metaData.excerpt || metaData.metaDescription,
|
||||
'twitter:url': metaData.canonicalUrl,
|
||||
'twitter:image': metaData.coverImage.url,
|
||||
'twitter:label1': metaData.authorName ? 'Written by' : undefined,
|
||||
|
|
29
core/server/data/migrations/versions/1.3/1-post-excerpt.js
Normal file
29
core/server/data/migrations/versions/1.3/1-post-excerpt.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const Promise = require('bluebird'),
|
||||
logging = require('../../../../logging'),
|
||||
commands = require('../../../schema').commands,
|
||||
table = 'posts',
|
||||
column = 'custom_excerpt',
|
||||
message = 'Adding column: ' + table + '.' + column;
|
||||
|
||||
module.exports = function addCustomExcerptColumn(options) {
|
||||
let transacting = options.transacting;
|
||||
|
||||
return transacting.schema.hasTable(table)
|
||||
.then(function (exists) {
|
||||
if (!exists) {
|
||||
return Promise.reject(new Error('Table does not exist!'));
|
||||
}
|
||||
|
||||
return transacting.schema.hasColumn(table, column);
|
||||
})
|
||||
.then(function (exists) {
|
||||
if (exists) {
|
||||
logging.warn(message);
|
||||
}
|
||||
|
||||
logging.info(message);
|
||||
return commands.addColumn(table, column, transacting);
|
||||
});
|
||||
};
|
|
@ -22,7 +22,8 @@ module.exports = {
|
|||
updated_at: {type: 'dateTime', nullable: true},
|
||||
updated_by: {type: 'string', maxlength: 24, nullable: true},
|
||||
published_at: {type: 'dateTime', nullable: true},
|
||||
published_by: {type: 'string', maxlength: 24, nullable: true}
|
||||
published_by: {type: 'string', maxlength: 24, nullable: true},
|
||||
custom_excerpt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}}
|
||||
},
|
||||
users: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
|
|
|
@ -11,7 +11,8 @@ var proxy = require('./proxy'),
|
|||
getMetaDataExcerpt = proxy.metaData.getMetaDataExcerpt;
|
||||
|
||||
module.exports = function excerpt(options) {
|
||||
var truncateOptions = (options || {}).hash || {};
|
||||
var truncateOptions = (options || {}).hash || {},
|
||||
excerptText = this.custom_excerpt ? String(this.custom_excerpt) : String(this.html);
|
||||
|
||||
truncateOptions = _.pick(truncateOptions, ['words', 'characters']);
|
||||
_.keys(truncateOptions).map(function (key) {
|
||||
|
@ -19,6 +20,6 @@ module.exports = function excerpt(options) {
|
|||
});
|
||||
|
||||
return new SafeString(
|
||||
getMetaDataExcerpt(String(this.html), truncateOptions)
|
||||
getMetaDataExcerpt(excerptText, truncateOptions)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -62,6 +62,7 @@ describe('Post Model', function () {
|
|||
firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name);
|
||||
firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name);
|
||||
firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name);
|
||||
firstPost.custom_excerpt.should.equal(DataGenerator.Content.posts[0].custom_excerpt);
|
||||
|
||||
if (options.formats) {
|
||||
if (options.formats.indexOf('mobiledoc') !== -1) {
|
||||
|
@ -442,6 +443,44 @@ describe('Post Model', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('[failure] custom excerpt soft limit reached', function (done) {
|
||||
var postId = testUtils.DataGenerator.Content.posts[0].id;
|
||||
|
||||
PostModel.findOne({id: postId}).then(function (results) {
|
||||
var post;
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.id.should.equal(postId);
|
||||
|
||||
return PostModel.edit({
|
||||
custom_excerpt: new Array(302).join('a')
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then(function () {
|
||||
done(new Error('expected validation error'));
|
||||
}).catch(function (err) {
|
||||
(err[0] instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('[success] custom excerpt soft limit respected', function (done) {
|
||||
var postId = testUtils.DataGenerator.Content.posts[0].id;
|
||||
|
||||
PostModel.findOne({id: postId}).then(function (results) {
|
||||
var post;
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.id.should.equal(postId);
|
||||
|
||||
return PostModel.edit({
|
||||
custom_excerpt: new Array(300).join('a')
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then(function (edited) {
|
||||
edited.get('custom_excerpt').length.should.eql(299);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can change title to number', function (done) {
|
||||
var postId = testUtils.DataGenerator.Content.posts[0].id;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('getMetaDescription', function () {
|
|||
description.should.equal('');
|
||||
});
|
||||
|
||||
it('should return data author bio if on root context contains author', function () {
|
||||
it('should not return meta description for author if on root context contains author and no meta description provided', function () {
|
||||
var description = getMetaDescription({
|
||||
author: {
|
||||
bio: 'Just some hack building code to make the world better.'
|
||||
|
@ -24,7 +24,19 @@ describe('getMetaDescription', function () {
|
|||
}, {
|
||||
context: ['author']
|
||||
});
|
||||
description.should.equal('Just some hack building code to make the world better.');
|
||||
description.should.equal('');
|
||||
});
|
||||
|
||||
it('should return meta description for author if on root context contains author and meta description provided', function () {
|
||||
var description = getMetaDescription({
|
||||
author: {
|
||||
bio: 'Just some hack building code to make the world better.',
|
||||
meta_description: 'Author meta description.'
|
||||
}
|
||||
}, {
|
||||
context: ['author']
|
||||
});
|
||||
description.should.equal('Author meta description.');
|
||||
});
|
||||
|
||||
it('should return data tag meta description if on root context contains tag', function () {
|
||||
|
@ -38,7 +50,7 @@ describe('getMetaDescription', function () {
|
|||
description.should.equal('Best tag ever!');
|
||||
});
|
||||
|
||||
it('should return data tag description if no meta description for tag', function () {
|
||||
it('should not return data tag description if no meta description for tag', function () {
|
||||
var description = getMetaDescription({
|
||||
tag: {
|
||||
meta_description: '',
|
||||
|
@ -47,7 +59,7 @@ describe('getMetaDescription', function () {
|
|||
}, {
|
||||
context: ['tag']
|
||||
});
|
||||
description.should.equal('The normal description');
|
||||
description.should.equal('');
|
||||
});
|
||||
|
||||
it('should return data post meta description if on root context contains post', function () {
|
||||
|
|
|
@ -38,7 +38,8 @@ describe('getSchema', function () {
|
|||
}
|
||||
},
|
||||
keywords: ['one', 'two', 'tag'],
|
||||
metaDescription: 'Post meta description'
|
||||
metaDescription: 'Post meta description',
|
||||
excerpt: 'Custom excerpt for description'
|
||||
}, data = {
|
||||
context: ['post'],
|
||||
post: {
|
||||
|
@ -57,7 +58,6 @@ describe('getSchema', function () {
|
|||
'@type': 'Article',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
description: 'My author bio.',
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'http://mysite.com/author/image/url/me.jpg',
|
||||
|
@ -74,7 +74,7 @@ describe('getSchema', function () {
|
|||
},
|
||||
dateModified: '2016-01-21T22:13:05.412Z',
|
||||
datePublished: '2015-12-25T05:35:01.234Z',
|
||||
description: 'Post meta description',
|
||||
description: 'Custom excerpt for description',
|
||||
headline: 'Post Title',
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
|
@ -137,7 +137,8 @@ describe('getSchema', function () {
|
|||
}
|
||||
},
|
||||
keywords: ['one', 'two', 'tag'],
|
||||
metaDescription: 'Post meta description'
|
||||
metaDescription: 'Post meta description',
|
||||
excerpt: 'Post meta description'
|
||||
}, data = {
|
||||
context: ['amp', 'post'],
|
||||
post: {
|
||||
|
@ -160,7 +161,6 @@ describe('getSchema', function () {
|
|||
'@type': 'Article',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
description: 'My author bio.',
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'http://mysite.com/author/image/url/me.jpg',
|
||||
|
@ -220,7 +220,8 @@ describe('getSchema', function () {
|
|||
modifiedDate: '2016-01-21T22:13:05.412Z',
|
||||
coverImage: undefined,
|
||||
keywords: [],
|
||||
metaDescription: 'Post meta description'
|
||||
metaDescription: '',
|
||||
excerpt: 'Post meta description'
|
||||
}, data = {
|
||||
context: ['post'],
|
||||
post: {
|
||||
|
@ -284,7 +285,8 @@ describe('getSchema', function () {
|
|||
url: 'http://mysite.com/content/image/mypostcoverimage.jpg'
|
||||
},
|
||||
keywords: ['one', 'two', 'tag'],
|
||||
metaDescription: 'Post meta description'
|
||||
metaDescription: 'Post meta description',
|
||||
excerpt: 'Post meta description'
|
||||
}, data = {
|
||||
context: ['post'],
|
||||
post: {
|
||||
|
@ -293,7 +295,8 @@ describe('getSchema', function () {
|
|||
website: 'http://myblogsite.com/',
|
||||
bio: 'My author bio.',
|
||||
facebook: 'testuser',
|
||||
twitter: '@testuser'
|
||||
twitter: '@testuser',
|
||||
metaDescription: 'My author bio.'
|
||||
}
|
||||
}
|
||||
}, schema = getSchema(metadata, data);
|
||||
|
|
|
@ -19,7 +19,7 @@ var should = require('should'), // jshint ignore:line
|
|||
// both of which are required for migrations to work properly.
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
var currentSchemaHash = 'b613bca0f20e02360487a3c17a9ffcc1',
|
||||
var currentSchemaHash = '903948c99647dd2ba480ab5e97611032',
|
||||
currentFixturesHash = '6e533f365835744545c53b788a08d8c6';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
|
|
|
@ -85,4 +85,22 @@ describe('{{excerpt}} Helper', function () {
|
|||
should.exist(rendered);
|
||||
rendered.string.should.equal(expected);
|
||||
});
|
||||
|
||||
it('uses custom excerpt if provided instead of truncating html', function () {
|
||||
var html = '<p>Hello <strong>World! It\'s me!</strong></p>',
|
||||
customExcerpt = 'My Custom Excerpt wins!',
|
||||
expected = 'My Custo',
|
||||
rendered = (
|
||||
helpers.excerpt.call(
|
||||
{
|
||||
html: html,
|
||||
custom_excerpt: customExcerpt
|
||||
},
|
||||
{hash: {characters: '8'}}
|
||||
)
|
||||
);
|
||||
|
||||
should.exist(rendered);
|
||||
rendered.string.should.equal(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -200,7 +200,7 @@ describe('{{ghost_head}} helper', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('tag first page without meta description uses tag description, and title if no meta title', function (done) {
|
||||
it('tag first page without meta data if no meta title and meta description, but model description provided', function (done) {
|
||||
var tag = {
|
||||
meta_description: '',
|
||||
description: 'tag description',
|
||||
|
@ -216,16 +216,16 @@ describe('{{ghost_head}} helper', function () {
|
|||
should.exist(rendered);
|
||||
rendered.string.should.match(/<link rel="shortcut icon" href="\/favicon.ico" type="image\/x-icon" \/>/);
|
||||
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:82832\/tag\/tagtitle\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="description" content="tag description" \/>/);
|
||||
rendered.string.should.not.match(/<meta name="description"/);
|
||||
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.not.match(/<meta property="og:description"/);
|
||||
rendered.string.should.match(/<meta property="og:url" content="http:\/\/localhost:82832\/tag\/tagtitle\/" \/>/);
|
||||
rendered.string.should.match(/<meta property="og:image" content="http:\/\/localhost:82832\/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.not.match(/<meta name="twitter:description"/);
|
||||
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/localhost:82832\/tag\/tagtitle\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:image" content="http:\/\/localhost:82832\/content\/images\/tag-image.png" \/>/);
|
||||
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
|
||||
|
@ -237,13 +237,13 @@ describe('{{ghost_head}} helper', function () {
|
|||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/tag\/tagtitle\/"/);
|
||||
rendered.string.should.match(/"image": "http:\/\/localhost:82832\/content\/images\/tag-image.png"/);
|
||||
rendered.string.should.match(/"name": "tagtitle"/);
|
||||
rendered.string.should.match(/"description": "tag description"/);
|
||||
rendered.string.should.not.match(/"description":/);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('tag first page with meta and model description returns no description fields', function (done) {
|
||||
it('tag first page without meta and model description returns no description fields', function (done) {
|
||||
var tag = {
|
||||
meta_description: '',
|
||||
name: 'tagtitle',
|
||||
|
@ -310,16 +310,16 @@ describe('{{ghost_head}} helper', function () {
|
|||
should.exist(rendered);
|
||||
rendered.string.should.match(/<link rel="shortcut icon" href="\/favicon.ico" type="image\/x-icon" \/>/);
|
||||
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:82832\/author\/AuthorName\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="description" content="Author bio" \/>/);
|
||||
rendered.string.should.not.match(/<meta name="description"/);
|
||||
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.not.match(/<meta property="og:description"/);
|
||||
rendered.string.should.match(/<meta property="og:url" content="http:\/\/localhost:82832\/author\/AuthorName\/" \/>/);
|
||||
rendered.string.should.match(/<meta property="og:image" content="http:\/\/localhost:82832\/content\/images\/author-cover-image.png" \/>/);
|
||||
rendered.string.should.match(/<meta property="article:author" content="https:\/\/www.facebook.com\/testuser\" \/>/);
|
||||
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.not.match(/<meta name="twitter:description"/);
|
||||
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/localhost:82832\/author\/AuthorName\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:creator" content="@testuser" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:image" content="http:\/\/localhost:82832\/content\/images\/author-cover-image.png" \/>/);
|
||||
|
@ -332,7 +332,7 @@ describe('{{ghost_head}} helper', function () {
|
|||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/author\/AuthorName\/"/);
|
||||
rendered.string.should.match(/"image": "http:\/\/localhost:82832\/content\/images\/author-cover-image.png"/);
|
||||
rendered.string.should.match(/"name": "Author name"/);
|
||||
rendered.string.should.match(/"description": "Author bio"/);
|
||||
rendered.string.should.not.match(/"description":/);
|
||||
|
||||
author.should.eql(authorBk);
|
||||
|
||||
|
@ -448,7 +448,7 @@ describe('{{ghost_head}} helper', function () {
|
|||
rendered.string.should.match(/"image\": \"http:\/\/localhost:82832\/content\/images\/test-author-image.png\"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/author\/Author\/"/);
|
||||
rendered.string.should.match(/"sameAs": \[\n "http:\/\/authorwebsite.com",\n "https:\/\/www.facebook.com\/testuser",\n "https:\/\/twitter.com\/testuser"\n \]/);
|
||||
rendered.string.should.match(/"description": "Author bio"/);
|
||||
rendered.string.should.not.match(/"description": "Author bio"/);
|
||||
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/post\/"/);
|
||||
rendered.string.should.match(re3);
|
||||
|
@ -466,6 +466,140 @@ describe('{{ghost_head}} helper', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('returns structured data on post page with custom excerpt for description and meta description', function (done) {
|
||||
var post = {
|
||||
meta_description: 'blog description',
|
||||
custom_excerpt: 'post custom excerpt',
|
||||
title: 'Welcome to Ghost',
|
||||
feature_image: '/content/images/test-image.png',
|
||||
published_at: moment('2008-05-31T19:18:15').toISOString(),
|
||||
updated_at: moment('2014-10-06T15:23:54').toISOString(),
|
||||
tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}],
|
||||
author: {
|
||||
name: 'Author name',
|
||||
url: 'http://testauthorurl.com',
|
||||
slug: 'Author',
|
||||
profile_image: '/content/images/test-author-image.png',
|
||||
website: 'http://authorwebsite.com',
|
||||
bio: 'Author bio',
|
||||
facebook: 'testuser',
|
||||
twitter: '@testuser'
|
||||
}
|
||||
}, postBk = _.cloneDeep(post);
|
||||
|
||||
helpers.ghost_head.call(
|
||||
{relativeUrl: '/post/', safeVersion: '0.3', 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.match(/<link rel="shortcut icon" href="\/favicon.ico" type="image\/x-icon" \/>/);
|
||||
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/<link rel="amphtml" href="http:\/\/localhost:82832\/post\/amp\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="description" content="blog description" \/>/);
|
||||
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="post custom excerpt" \/>/);
|
||||
rendered.string.should.match(/<meta property="og:url" content="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/<meta property="og:image" content="http:\/\/localhost:82832\/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 property="article:author" content="https:\/\/www.facebook.com\/testuser" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:description" content="post custom excerpt" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:image" content="http:\/\/localhost:82832\/content\/images\/test-image.png" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:creator" content="@testuser" \/>/);
|
||||
rendered.string.should.match(/"@context": "https:\/\/schema.org"/);
|
||||
rendered.string.should.match(/"@type": "Article"/);
|
||||
rendered.string.should.match(/"publisher": {/);
|
||||
rendered.string.should.match(/"@type": "Organization"/);
|
||||
rendered.string.should.match(/"name": "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:\/\/localhost:82832\/content\/images\/test-author-image.png\"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/author\/Author\/"/);
|
||||
rendered.string.should.match(/"sameAs": \[\n "http:\/\/authorwebsite.com",\n "https:\/\/www.facebook.com\/testuser",\n "https:\/\/twitter.com\/testuser"\n \]/);
|
||||
rendered.string.should.not.match(/"description": "Author bio"/);
|
||||
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/post\/"/);
|
||||
rendered.string.should.match(re3);
|
||||
rendered.string.should.match(re4);
|
||||
rendered.string.should.match(/"image": "http:\/\/localhost:82832\/content\/images\/test-image.png"/);
|
||||
rendered.string.should.match(/"keywords": "tag1, tag2, tag3"/);
|
||||
rendered.string.should.match(/"description": "post custom excerpt"/);
|
||||
rendered.string.should.match(/"@context": "https:\/\/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:\/\/localhost:82832\/rss\/" \/>/);
|
||||
|
||||
post.should.eql(postBk);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('returns structured data on post page with fall back excerpt if no meta description provided', function (done) {
|
||||
var post = {
|
||||
meta_description: '',
|
||||
custom_excerpt: '',
|
||||
title: 'Welcome to Ghost',
|
||||
html: '<p>This is a short post</p>',
|
||||
author: {
|
||||
name: 'Author name',
|
||||
url: 'http://testauthorurl.com',
|
||||
slug: 'Author'
|
||||
}
|
||||
}, postBk = _.cloneDeep(post);
|
||||
|
||||
helpers.ghost_head.call(
|
||||
{relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post},
|
||||
{data: {root: {context: ['post']}}}
|
||||
).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.match(/<link rel="shortcut icon" href="\/favicon.ico" type="image\/x-icon" \/>/);
|
||||
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/<link rel="amphtml" href="http:\/\/localhost:82832\/post\/amp\/" \/>/);
|
||||
rendered.string.should.not.match(/<meta name="description" content="blog description" \/>/);
|
||||
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="This is a short post" \/>/);
|
||||
rendered.string.should.match(/<meta property="og:url" content="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:title" content="Welcome to Ghost" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:description" content="This is a short post" \/>/);
|
||||
rendered.string.should.match(/<meta name="twitter:url" content="http:\/\/localhost:82832\/post\/" \/>/);
|
||||
rendered.string.should.match(/"@context": "https:\/\/schema.org"/);
|
||||
rendered.string.should.match(/"@type": "Article"/);
|
||||
rendered.string.should.match(/"publisher": {/);
|
||||
rendered.string.should.match(/"@type": "Organization"/);
|
||||
rendered.string.should.match(/"name": "Ghost"/);
|
||||
rendered.string.should.match(/"author": {/);
|
||||
rendered.string.should.match(/"@type": "Person"/);
|
||||
rendered.string.should.match(/"name": "Author name"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/author\/Author\/"/);
|
||||
rendered.string.should.not.match(/"description": "Author bio"/);
|
||||
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/post\/"/);
|
||||
rendered.string.should.match(/"description": "This is a short post"/);
|
||||
rendered.string.should.match(/"@context": "https:\/\/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:\/\/localhost:82832\/rss\/" \/>/);
|
||||
|
||||
post.should.eql(postBk);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('returns structured data on AMP post page with author image and post cover image', function (done) {
|
||||
var post = {
|
||||
meta_description: 'blog description',
|
||||
|
@ -528,7 +662,7 @@ describe('{{ghost_head}} helper', function () {
|
|||
rendered.string.should.match(/"image\": \"http:\/\/localhost:82832\/content\/images\/test-author-image.png\"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/author\/Author\/"/);
|
||||
rendered.string.should.match(/"sameAs": \[\n "http:\/\/authorwebsite.com",\n "https:\/\/www.facebook.com\/testuser",\n "https:\/\/twitter.com\/testuser"\n \]/);
|
||||
rendered.string.should.match(/"description": "Author bio"/);
|
||||
rendered.string.should.not.match(/"description": "Author bio"/);
|
||||
rendered.string.should.match(/"headline": "Welcome to Ghost"/);
|
||||
rendered.string.should.match(/"url": "http:\/\/localhost:82832\/post\/"/);
|
||||
rendered.string.should.match(re3);
|
||||
|
|
|
@ -76,14 +76,14 @@ describe('{{meta_description}} helper', function () {
|
|||
String(rendered).should.equal('');
|
||||
});
|
||||
|
||||
it('returns correct description for an author page', function () {
|
||||
it('returns empty description for an author page', function () {
|
||||
var rendered = helpers.meta_description.call(
|
||||
{author: {bio: 'I am a Duck.'}},
|
||||
{data: {root: {context: ['author']}}}
|
||||
);
|
||||
|
||||
should.exist(rendered);
|
||||
String(rendered).should.equal('I am a Duck.');
|
||||
String(rendered).should.equal('');
|
||||
});
|
||||
|
||||
it('returns empty description for a paginated author page', function () {
|
||||
|
|
|
@ -31,7 +31,8 @@ DataGenerator.Content = {
|
|||
title: "HTML Ipsum",
|
||||
slug: "html-ipsum",
|
||||
mobiledoc: DataGenerator.markdownToMobiledoc("<h1>HTML Ipsum Presents</h1><p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href=\\\"#\\\">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p><h2>Header Level 2</h2><ol><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ol><blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote><h3>Header Level 3</h3><ul><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ul><pre><code>#header h1 a{display: block;width: 300px;height: 80px;}</code></pre>"),
|
||||
published_at: new Date("2015-01-01")
|
||||
published_at: new Date("2015-01-01"),
|
||||
custom_excerpt: 'This is my custom excerpt!'
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"jsdom": "9.12.0",
|
||||
"jsonpath": "0.2.11",
|
||||
"knex": "0.12.9",
|
||||
"knex-migrator": "2.0.16",
|
||||
"knex-migrator": "2.1.3",
|
||||
"lodash": "4.17.4",
|
||||
"markdown-it": "8.3.1",
|
||||
"markdown-it-footnote": "3.0.1",
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -1652,7 +1652,7 @@ fs-extra@0.26.2:
|
|||
path-is-absolute "^1.0.0"
|
||||
rimraf "^2.2.8"
|
||||
|
||||
fs-extra@3.0.1:
|
||||
fs-extra@3.0.1, fs-extra@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
|
||||
dependencies:
|
||||
|
@ -1748,7 +1748,7 @@ ghost-gql@0.0.6:
|
|||
dependencies:
|
||||
lodash "^4.17.4"
|
||||
|
||||
ghost-ignition@2.8.11, ghost-ignition@^2.8.2, ghost-ignition@^2.8.7:
|
||||
ghost-ignition@2.8.11, ghost-ignition@^2.8.7:
|
||||
version "2.8.11"
|
||||
resolved "https://registry.yarnpkg.com/ghost-ignition/-/ghost-ignition-2.8.11.tgz#38a018ca2b63bc57e9f2c9037d45b4714b66eba0"
|
||||
dependencies:
|
||||
|
@ -1764,6 +1764,23 @@ ghost-ignition@2.8.11, ghost-ignition@^2.8.2, ghost-ignition@^2.8.7:
|
|||
prettyjson "1.1.3"
|
||||
uuid "^3.0.0"
|
||||
|
||||
ghost-ignition@^2.8.12:
|
||||
version "2.8.12"
|
||||
resolved "https://registry.yarnpkg.com/ghost-ignition/-/ghost-ignition-2.8.12.tgz#e89e36e7a10a03daf04cb35c20c6c369fbc556f2"
|
||||
dependencies:
|
||||
bunyan "1.8.5"
|
||||
bunyan-loggly "1.1.0"
|
||||
caller "1.0.1"
|
||||
debug "^2.2.0"
|
||||
find-root "1.0.0"
|
||||
fs-extra "^3.0.1"
|
||||
json-stringify-safe "5.0.1"
|
||||
lodash "^4.16.4"
|
||||
moment "^2.15.2"
|
||||
nconf "0.8.4"
|
||||
prettyjson "1.1.3"
|
||||
uuid "^3.0.0"
|
||||
|
||||
ghost-storage-base@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ghost-storage-base/-/ghost-storage-base-0.0.1.tgz#b31b57d2e54574a96153a54bf2e9ea599f12bec8"
|
||||
|
@ -2905,14 +2922,14 @@ klaw@^1.0.0:
|
|||
optionalDependencies:
|
||||
graceful-fs "^4.1.9"
|
||||
|
||||
knex-migrator@2.0.16:
|
||||
version "2.0.16"
|
||||
resolved "https://registry.yarnpkg.com/knex-migrator/-/knex-migrator-2.0.16.tgz#6ac051d4e0fb2a8b633f8aab8b4d0019ecd4dd65"
|
||||
knex-migrator@2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/knex-migrator/-/knex-migrator-2.1.3.tgz#6b78df8571eb1d47942f767cfeb9fc29909bb56f"
|
||||
dependencies:
|
||||
bluebird "^3.4.6"
|
||||
commander "2.9.0"
|
||||
debug "^2.2.0"
|
||||
ghost-ignition "^2.8.2"
|
||||
ghost-ignition "^2.8.12"
|
||||
knex "^0.12.8"
|
||||
lodash "^4.16.4"
|
||||
resolve "1.1.7"
|
||||
|
|
Loading…
Add table
Reference in a new issue