0
Fork 0
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:
Katharina Irrgang 2017-08-01 12:39:34 +04:00 committed by Aileen Nowak
parent d8fb7ce7f6
commit 7845617607
17 changed files with 323 additions and 50 deletions

View file

@ -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();

View file

@ -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) {

View file

@ -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),

View file

@ -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,

View 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);
});
};

View file

@ -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},

View file

@ -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)
);
};

View file

@ -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;

View file

@ -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 () {

View file

@ -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);

View file

@ -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,

View file

@ -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);
});
});

View file

@ -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);

View file

@ -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 () {

View file

@ -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(),

View file

@ -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",

View file

@ -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"