mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Added Canonical URL support to posts&pages in Admin & Content API v2 (#10594)
refs #10593 - Added `canonical_url` field to post&pages resources in Admin & Content APIs - Support for canonical URL on metadata layer (used in {{ghost_head}} helper) - Made sure the new field is not accessible from API v0.1 - Added handling same domain relative and absolute URLs
This commit is contained in:
parent
c1abcc8dc6
commit
34fad7eaaf
15 changed files with 167 additions and 7 deletions
|
@ -1,13 +1,26 @@
|
|||
const _ = require('lodash');
|
||||
const {absoluteToRelative, getBlogUrl, STATIC_IMAGE_URL_PREFIX} = require('../../../../../../services/url/utils');
|
||||
|
||||
const handleCanonicalUrl = (url) => {
|
||||
const blogDomain = getBlogUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const absolute = url.replace(/^http(s?):\/\//, '');
|
||||
|
||||
if (absolute.startsWith(blogDomain)) {
|
||||
return absoluteToRelative(url, {withoutSubdirectory: true});
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const handleImageUrl = (imageUrl) => {
|
||||
const blogDomain = getBlogUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
|
||||
const imagePathRe = new RegExp(`^${blogDomain}/${STATIC_IMAGE_URL_PREFIX}`);
|
||||
|
||||
if (imagePathRe.test(imageUrlAbsolute)) {
|
||||
return absoluteToRelative(imageUrl);
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
|
@ -45,6 +58,10 @@ const forPost = (attrs, options) => {
|
|||
attrs.twitter_image = handleImageUrl(attrs.twitter_image);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (options && options.withRelated) {
|
||||
options.withRelated.forEach((relation) => {
|
||||
if (relation === 'tags' && attrs.tags) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const _ = require('lodash');
|
||||
const utils = require('../../../index');
|
||||
const url = require('./url');
|
||||
const date = require('./date');
|
||||
|
@ -25,7 +26,11 @@ const mapTag = (model, frame) => {
|
|||
};
|
||||
|
||||
const mapPost = (model, frame) => {
|
||||
const jsonModel = model.toJSON(frame.options);
|
||||
const extendedOptions = Object.assign(_.cloneDeep(frame.options), {
|
||||
extraProperties: ['canonical_url']
|
||||
});
|
||||
|
||||
const jsonModel = model.toJSON(extendedOptions);
|
||||
|
||||
url.forPost(model.id, jsonModel, frame.options);
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ const forPost = (id, attrs, options) => {
|
|||
attrs.twitter_image = urlService.utils.urlFor('image', {image: attrs.twitter_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = urlService.utils.relativeToAbsolute(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (attrs.html) {
|
||||
const urlOptions = {
|
||||
assetsOnly: true
|
||||
|
|
|
@ -103,6 +103,11 @@
|
|||
"type": ["string", "null"],
|
||||
"maxLength": 100
|
||||
},
|
||||
"canonical_url": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"authors": {
|
||||
"$ref": "#/definitions/page-authors"
|
||||
},
|
||||
|
|
|
@ -103,6 +103,11 @@
|
|||
"type": ["string", "null"],
|
||||
"maxLength": 100
|
||||
},
|
||||
"canonical_url": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"authors": {
|
||||
"$ref": "#/definitions/post-authors"
|
||||
},
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
var urlService = require('../../services/url'),
|
||||
getUrl = require('./url');
|
||||
const _ = require('lodash');
|
||||
const urlService = require('../../services/url');
|
||||
const getUrl = require('./url');
|
||||
|
||||
function getCanonicalUrl(data) {
|
||||
var url = urlService.utils.urlJoin(urlService.utils.urlFor('home', true), getUrl(data, false));
|
||||
if ((_.includes(data.context, 'post') || _.includes(data.context, 'page'))
|
||||
&& data.post && data.post.canonical_url) {
|
||||
return data.post.canonical_url;
|
||||
}
|
||||
|
||||
let url = urlService.utils.urlJoin(urlService.utils.urlFor('home', true), getUrl(data, false));
|
||||
|
||||
if (url.indexOf('/amp/')) {
|
||||
url = url.replace(/\/amp\/$/i, '/');
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
const Promise = require('bluebird'),
|
||||
common = require('../../../../lib/common'),
|
||||
commands = require('../../../schema').commands,
|
||||
table = 'posts',
|
||||
columns = ['canonical_url'],
|
||||
_private = {};
|
||||
|
||||
_private.handle = function handle(options) {
|
||||
let type = options.type,
|
||||
isAdding = type === 'Adding',
|
||||
operation = isAdding ? commands.addColumn : commands.dropColumn;
|
||||
|
||||
return function (options) {
|
||||
let connection = options.connection;
|
||||
|
||||
return connection.schema.hasTable(table)
|
||||
.then(function (exists) {
|
||||
if (!exists) {
|
||||
return Promise.reject(new Error('Table does not exist!'));
|
||||
}
|
||||
|
||||
return Promise.each(columns, function (column) {
|
||||
return connection.schema.hasColumn(table, column)
|
||||
.then(function (exists) {
|
||||
if (exists && isAdding || !exists && !isAdding) {
|
||||
common.logging.warn(`${type} column ${table}.${column}`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
common.logging.info(`${type} column ${table}.${column}`);
|
||||
return operation(table, column, connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.up = _private.handle({type: 'Adding'});
|
||||
module.exports.down = _private.handle({type: 'Dropping'});
|
|
@ -56,7 +56,8 @@ module.exports = {
|
|||
twitter_image: {type: 'string', maxlength: 2000, nullable: true},
|
||||
twitter_title: {type: 'string', maxlength: 300, nullable: true},
|
||||
twitter_description: {type: 'string', maxlength: 500, nullable: true},
|
||||
custom_template: {type: 'string', maxlength: 100, nullable: true}
|
||||
custom_template: {type: 'string', maxlength: 100, nullable: true},
|
||||
canonical_url: {type: 'text', maxlength: 2000, nullable: true}
|
||||
},
|
||||
users: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
|
|
|
@ -572,6 +572,13 @@ Post = ghostBookshelf.Model.extend({
|
|||
// CASE: never expose the revisions
|
||||
delete attrs.mobiledoc_revisions;
|
||||
|
||||
// expose canonical_url only for API v2 calls
|
||||
// NOTE: this can be removed when API v0.1 is dropped. A proper solution for field
|
||||
// differences on resources like this would be an introduction of API output schema
|
||||
if (!_.get(unfilteredOptions, 'extraProperties', []).includes('canonical_url')) {
|
||||
delete attrs.canonical_url;
|
||||
}
|
||||
|
||||
// If the current column settings allow it...
|
||||
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
||||
// ... attach a computed property of primary_tag which is the first tag if it is public, else null
|
||||
|
|
|
@ -460,11 +460,20 @@ function absoluteToRelative(urlToModify, options) {
|
|||
return relativePath;
|
||||
}
|
||||
|
||||
function relativeToAbsolute(url) {
|
||||
if (!url.startsWith('/') || url.startsWith('//')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return createUrl(url, true);
|
||||
}
|
||||
|
||||
function deduplicateDoubleSlashes(url) {
|
||||
return url.replace(/\/\//g, '/');
|
||||
}
|
||||
|
||||
module.exports.absoluteToRelative = absoluteToRelative;
|
||||
module.exports.relativeToAbsolute = relativeToAbsolute;
|
||||
module.exports.makeAbsoluteUrls = makeAbsoluteUrls;
|
||||
module.exports.getProtectedSlugs = getProtectedSlugs;
|
||||
module.exports.getSubdir = getSubdir;
|
||||
|
|
|
@ -22,6 +22,7 @@ const expectedProperties = {
|
|||
.without('mobiledoc', 'plaintext')
|
||||
// swaps author_id to author, and always returns computed properties: url, comment_id, primary_tag, primary_author
|
||||
.without('author_id').concat('author', 'url', 'primary_tag', 'primary_author')
|
||||
.without('canonical_url')
|
||||
.value(),
|
||||
user: {
|
||||
default: _(schema.users).keys().without('password').without('ghost_auth_access_token').value(),
|
||||
|
|
|
@ -151,6 +151,32 @@ describe('Posts API', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('canonical_url', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({
|
||||
posts: [{
|
||||
canonical_url: `/canonical/url`,
|
||||
updated_at: res.body.posts[0].updated_at
|
||||
}]
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200);
|
||||
})
|
||||
.then((res) => {
|
||||
should.exist(res.body.posts);
|
||||
should.exist(res.body.posts[0].canonical_url);
|
||||
res.body.posts[0].canonical_url.should.equal(`${config.get('url')}/canonical/url`);
|
||||
});
|
||||
});
|
||||
|
||||
it('update dates & x_by', function () {
|
||||
const post = {
|
||||
created_by: ObjectId.generate(),
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('getCanonicalUrl', function () {
|
|||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should return canonical url', function () {
|
||||
it('should return default canonical url', function () {
|
||||
const post = testUtils.DataGenerator.forKnex.createPost();
|
||||
|
||||
getUrlStub.withArgs(post, false).returns('/post-url/');
|
||||
|
@ -36,6 +36,17 @@ describe('getCanonicalUrl', function () {
|
|||
getUrlStub.calledOnce.should.be.true();
|
||||
});
|
||||
|
||||
it('should return canonical url field if present', function () {
|
||||
const post = testUtils.DataGenerator.forKnex.createPost({canonical_url: 'https://example.com/canonical'});
|
||||
|
||||
getCanonicalUrl({
|
||||
context: ['post'],
|
||||
post: post
|
||||
}).should.eql('https://example.com/canonical');
|
||||
|
||||
getUrlStub.called.should.equal(false);
|
||||
});
|
||||
|
||||
it('should return canonical url for amp post without /amp/ in url', function () {
|
||||
const post = testUtils.DataGenerator.forKnex.createPost();
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ var should = require('should'),
|
|||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = '7c5d34376392d01c274700350de228c1';
|
||||
const currentSchemaHash = 'fda0398e93a74b2dc435cb4c026679ba';
|
||||
const currentFixturesHash = '42e15796b3c9bdcf0d0ec7eb66a1abf5';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
|
|
|
@ -43,6 +43,30 @@ describe('Url', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('relativeToAbsolute', function () {
|
||||
it('default', function () {
|
||||
configUtils.set('url', 'http://myblog.com/');
|
||||
urlService.utils.relativeToAbsolute('/test/').should.eql('http://myblog.com/test/');
|
||||
});
|
||||
|
||||
it('with subdir', function () {
|
||||
configUtils.set('url', 'http://myblog.com/blog/');
|
||||
urlService.utils.relativeToAbsolute('/test/').should.eql('http://myblog.com/blog/test/');
|
||||
});
|
||||
|
||||
it('should not convert absolute url', function () {
|
||||
urlService.utils.relativeToAbsolute('http://anotherblog.com/blog/').should.eql('http://anotherblog.com/blog/');
|
||||
});
|
||||
|
||||
it('should not convert absolute url', function () {
|
||||
urlService.utils.relativeToAbsolute('http://anotherblog.com/blog/').should.eql('http://anotherblog.com/blog/');
|
||||
});
|
||||
|
||||
it('should not convert schemeless url', function () {
|
||||
urlService.utils.relativeToAbsolute('//anotherblog.com/blog/').should.eql('//anotherblog.com/blog/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProtectedSlugs', function () {
|
||||
it('defaults', function () {
|
||||
urlService.utils.getProtectedSlugs().should.eql(['ghost', 'rss', 'amp']);
|
||||
|
|
Loading…
Add table
Reference in a new issue