Skip to content

Commit

Permalink
✨ Facebook and Twitter data per post feature (#8827)
Browse files Browse the repository at this point in the history
closes #8334

- adds title, image and description to structured data to be rendered as open graph and twitter data.
- if meta title and description for a post exists already, the custom structured data will overwrite those for `og:` and `twitter:` data. `JSON-LD` (Schema.org`) is not affected and will stay the same.
- adds tests
- adds new og and twitter fields to schema incl. migration
  • Loading branch information
aileen authored and kirrg001 committed Aug 3, 2017
1 parent d73133d commit cfbb7f6
Show file tree
Hide file tree
Showing 17 changed files with 705 additions and 51 deletions.
12 changes: 10 additions & 2 deletions core/server/data/meta/description.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
var _ = require('lodash'),
settingsCache = require('../../settings/cache');

function getDescription(data, root) {
function getDescription(data, root, options) {
var description = '',
postSdDescription,
context = root ? root.context : null,
blogDescription = settingsCache.get('description');

options = options ? options : {};

// We only return meta_description if provided. Only exception is the Blog
// description, which doesn't rely on meta_description.
if (data.meta_description) {
Expand All @@ -22,7 +25,12 @@ function getDescription(data, root) {
} else if (_.includes(context, 'tag') && data.tag) {
description = data.tag.meta_description || '';
} else if ((_.includes(context, 'post') || _.includes(context, 'page')) && data.post) {
description = data.post.meta_description || '';
if (options && options.property) {
postSdDescription = options.property + '_description';
description = data.post[postSdDescription] || '';
} else {
description = data.post.meta_description || '';
}
}

return (description || '').trim();
Expand Down
2 changes: 2 additions & 0 deletions core/server/data/meta/image-dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function getImageDimensions(metaData) {
var fetch = {
coverImage: getCachedImageSizeFromUrl(metaData.coverImage.url),
authorImage: getCachedImageSizeFromUrl(metaData.authorImage.url),
ogImage: getCachedImageSizeFromUrl(metaData.ogImage.url),
// CASE: check if logo has hard coded image dimension. In that case it's an `ico` file, which
// is not supported by `image-size` and would produce an error
logo: metaData.blog.logo && metaData.blog.logo.dimensions ? metaData.blog.logo.dimensions : getCachedImageSizeFromUrl(metaData.blog.logo.url)
Expand All @@ -24,6 +25,7 @@ function getImageDimensions(metaData) {
imageObj = {
coverImage: resolve.coverImage,
authorImage: resolve.authorImage,
ogImage: resolve.ogImage,
logo: resolve.logo
};

Expand Down
10 changes: 10 additions & 0 deletions core/server/data/meta/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var Promise = require('bluebird'),
getPublishedDate = require('./published_date'),
getModifiedDate = require('./modified_date'),
getOgType = require('./og_type'),
getOgImage = require('./og_image'),
getTwitterImage = require('./twitter_image'),
getStructuredData = require('./structured_data'),
getSchema = require('./schema'),
getExcerpt = require('./excerpt');
Expand All @@ -41,6 +43,14 @@ function getMetaData(data, root) {
authorImage: {
url: getAuthorImage(data, true)
},
ogImage: {
url: getOgImage(data, true)
},
ogTitle: getTitle(data, root, {property: 'og'}),
ogDescription: getDescription(data, root, {property: 'og'}),
twitterImage: getTwitterImage(data, true),
twitterTitle: getTitle(data, root, {property: 'twitter'}),
twitterDescription: getDescription(data, root, {property: 'twitter'}),
authorFacebook: getAuthorFacebook(data),
creatorTwitter: getCreatorTwitter(data),
keywords: getKeywords(data),
Expand Down
20 changes: 20 additions & 0 deletions core/server/data/meta/og_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var utils = require('../../utils'),
getContextObject = require('./context_object.js'),
_ = require('lodash');

function getOgImage(data) {
var context = data.context ? data.context : null,
contextObject = getContextObject(data, context);

if (_.includes(context, 'post') || _.includes(context, 'page') || _.includes(context, 'amp')) {
if (contextObject.og_image) {
return utils.url.urlFor('image', {image: contextObject.og_image}, true);
} else if (contextObject.feature_image) {
return utils.url.urlFor('image', {image: contextObject.feature_image}, true);
}
}

return null;
}

module.exports = getOgImage;
17 changes: 10 additions & 7 deletions core/server/data/meta/structured_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ function getStructuredData(metaData) {
structuredData = {
'og:site_name': metaData.blog.title,
'og:type': metaData.ogType,
'og:title': metaData.metaTitle,
'og:title': metaData.ogTitle || metaData.metaTitle,
// 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:description': metaData.ogDescription || metaData.excerpt || metaData.metaDescription,
'og:url': metaData.canonicalUrl,
'og:image': metaData.coverImage.url,
'og:image': metaData.ogImage.url || metaData.coverImage.url,
'article:published_time': metaData.publishedDate,
'article:modified_time': metaData.modifiedDate,
'article:tag': metaData.keywords,
'article:publisher': metaData.blog.facebook ? socialUrls.facebookUrl(metaData.blog.facebook) : undefined,
'article:author': metaData.authorFacebook ? socialUrls.facebookUrl(metaData.authorFacebook) : undefined,
'twitter:card': card,
'twitter:title': metaData.metaTitle,
'twitter:description': metaData.excerpt || metaData.metaDescription,
'twitter:title': metaData.twitterTitle || metaData.metaTitle,
'twitter:description': metaData.twitterDescription || metaData.excerpt || metaData.metaDescription,
'twitter:url': metaData.canonicalUrl,
'twitter:image': metaData.coverImage.url,
'twitter:image': metaData.twitterImage || metaData.coverImage.url,
'twitter:label1': metaData.authorName ? 'Written by' : undefined,
'twitter:data1': metaData.authorName,
'twitter:label2': metaData.keywords ? 'Filed under' : undefined,
Expand All @@ -36,7 +36,10 @@ function getStructuredData(metaData) {
'twitter:creator': metaData.creatorTwitter || undefined
};

if (metaData.coverImage.dimensions) {
if (metaData.ogImage.dimensions) {
structuredData['og:image:width'] = metaData.ogImage.dimensions.width;
structuredData['og:image:height'] = metaData.ogImage.dimensions.height;
} else if (metaData.coverImage.dimensions) {
structuredData['og:image:width'] = metaData.coverImage.dimensions.width;
structuredData['og:image:height'] = metaData.coverImage.dimensions.height;
}
Expand Down
12 changes: 10 additions & 2 deletions core/server/data/meta/title.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
var _ = require('lodash'),
settingsCache = require('../../settings/cache');

function getTitle(data, root) {
function getTitle(data, root, options) {
var title = '',
context = root ? root.context : null,
postSdTitle,
blogTitle = settingsCache.get('title'),
pagination = root ? root.pagination : null,
pageString = '';

options = options ? options : {};

if (pagination && pagination.total > 1) {
pageString = ' (Page ' + pagination.page + ')';
}
Expand All @@ -32,7 +35,12 @@ function getTitle(data, root) {
title = data.tag.meta_title || data.tag.name + ' - ' + blogTitle;
// Post title
} else if ((_.includes(context, 'post') || _.includes(context, 'page')) && data.post) {
title = data.post.meta_title || data.post.title;
if (options && options.property) {
postSdTitle = options.property + '_title';
title = data.post[postSdTitle] || '';
} else {
title = data.post.meta_title || data.post.title;
}
// Fallback
} else {
title = blogTitle + pageString;
Expand Down
20 changes: 20 additions & 0 deletions core/server/data/meta/twitter_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var utils = require('../../utils'),
getContextObject = require('./context_object.js'),
_ = require('lodash');

function getTwitterImage(data) {
var context = data.context ? data.context : null,
contextObject = getContextObject(data, context);

if (_.includes(context, 'post') || _.includes(context, 'page') || _.includes(context, 'amp')) {
if (contextObject.twitter_image) {
return utils.url.urlFor('image', {image: contextObject.twitter_image}, true);
} else if (contextObject.feature_image) {
return utils.url.urlFor('image', {image: contextObject.feature_image}, true);
}
}

return null;
}

module.exports = getTwitterImage;
100 changes: 100 additions & 0 deletions core/server/data/migrations/versions/1.5/1-og-twitter-post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';

const Promise = require('bluebird'),
logging = require('../../../../logging'),
commands = require('../../../schema').commands,
table = 'posts',
column1 = 'og_image',
column2 = 'og_title',
column3 = 'og_description',
column4 = 'twitter_image',
column5 = 'twitter_title',
column6 = 'twitter_description',
message1 = 'Adding column: ' + table + '.' + column1,
message2 = 'Adding column: ' + table + '.' + column2,
message3 = 'Adding column: ' + table + '.' + column3,
message4 = 'Adding column: ' + table + '.' + column4,
message5 = 'Adding column: ' + table + '.' + column5,
message6 = 'Adding column: ' + table + '.' + column6;

module.exports = function addCodeInjectionPostColumns(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, column1);
})
.then(function (exists) {
if (exists) {
logging.warn(message1);
return Promise.resolve();
}

logging.info(message1);
return commands.addColumn(table, column1, transacting);
})
.then(function () {
return transacting.schema.hasColumn(table, column2);
})
.then(function (exists) {
if (exists) {
logging.warn(message2);
return Promise.resolve();
}

logging.info(message2);
return commands.addColumn(table, column2, transacting);
})
.then(function () {
return transacting.schema.hasColumn(table, column3);
})
.then(function (exists) {
if (exists) {
logging.warn(message3);
return Promise.resolve();
}

logging.info(message3);
return commands.addColumn(table, column3, transacting);
})
.then(function () {
return transacting.schema.hasColumn(table, column4);
})
.then(function (exists) {
if (exists) {
logging.warn(message4);
return Promise.resolve();
}

logging.info(message4);
return commands.addColumn(table, column4, transacting);
})
.then(function () {
return transacting.schema.hasColumn(table, column5);
})
.then(function (exists) {
if (exists) {
logging.warn(message5);
return Promise.resolve();
}

logging.info(message5);
return commands.addColumn(table, column5, transacting);
})
.then(function () {
return transacting.schema.hasColumn(table, column6);
})
.then(function (exists) {
if (exists) {
logging.warn(message6);
return Promise.resolve();
}

logging.info(message6);
return commands.addColumn(table, column6, transacting);
});
};
8 changes: 7 additions & 1 deletion core/server/data/schema/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ module.exports = {
published_by: {type: 'string', maxlength: 24, nullable: true},
custom_excerpt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}},
codeinjection_head: {type: 'text', maxlength: 65535, nullable: true},
codeinjection_foot: {type: 'text', maxlength: 65535, nullable: true}
codeinjection_foot: {type: 'text', maxlength: 65535, nullable: true},
og_image: {type: 'string', maxlength: 2000, nullable: true},
og_title: {type: 'string', maxlength: 300, nullable: true},
og_description: {type: 'string', maxlength: 500, nullable: true},
twitter_image: {type: 'string', maxlength: 2000, nullable: true},
twitter_title: {type: 'string', maxlength: 300, nullable: true},
twitter_description: {type: 'string', maxlength: 500, nullable: true}
},
users: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
Expand Down
42 changes: 42 additions & 0 deletions core/test/unit/metadata/description_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,48 @@ describe('getMetaDescription', function () {
description.should.equal('Best post ever!');
});

it('should return OG data post meta description if on root context contains post', function () {
var description = getMetaDescription({
post: {
meta_description: 'Best post ever!',
og_description: 'My custom Facebook description!'
}
}, {
context: ['post']
}, {
property: 'og'
});
description.should.equal('My custom Facebook description!');
});

it('should not return data post meta description if on root context contains post and called with OG property', function () {
var description = getMetaDescription({
post: {
meta_description: 'Best post ever!',
og_description: ''
}
}, {
context: ['post']
}, {
property: 'og'
});
description.should.equal('');
});

it('should return Twitter data post meta description if on root context contains post', function () {
var description = getMetaDescription({
post: {
meta_description: 'Best post ever!',
twitter_description: 'My custom Twitter description!'
}
}, {
context: ['post']
}, {
property: 'twitter'
});
description.should.equal('My custom Twitter description!');
});

it('should return data post meta description if on root context contains post for an AMP post', function () {
var description = getMetaDescription({
post: {
Expand Down
Loading

0 comments on commit cfbb7f6

Please sign in to comment.