feat(layout): add structured data support

This commit is contained in:
ppoffice 2019-12-26 13:21:11 -05:00
parent ea66ff939d
commit 48fd263ea5
7 changed files with 147 additions and 15 deletions

View File

@ -18,7 +18,7 @@ module.exports = hexo => {
const ALTERNATIVE_CONFIG = {
post: getThemeConfig('.post'),
page: getThemeConfig('.page')
}
};
function getExtraConfig(source, reservedKeys) {
const result = {};
@ -48,7 +48,7 @@ module.exports = hexo => {
if (page) {
if ((page.layout !== 'page' || page.layout !== 'post') && ALTERNATIVE_CONFIG[page.layout]) {
// load alternative config if exists
locals.config = Object.assign({}, Object.getPrototypeOf(locals).config, ALTERNATIVE_CONFIG[page.layout])
locals.config = Object.assign({}, Object.getPrototypeOf(locals).config, ALTERNATIVE_CONFIG[page.layout]);
} else {
// site config already merged into theme config in hexo/lib/hexo/index.js#Hexo.prototype._generateLocals()
locals.config = Object.assign({}, Object.getPrototypeOf(locals).theme);

View File

@ -18,6 +18,9 @@
"open_graph": {
"$ref": "/misc/open_graph.json"
},
"structured_data": {
"$ref": "/misc/structured_data.json"
},
"meta": {
"$ref": "/misc/meta.json"
},

View File

@ -6,18 +6,18 @@
"properties": {
"title": {
"type": "string",
"description": "Page title (og:title)",
"description": "Page title (og:title) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"type": {
"type": "string",
"description": "Page type (og:type)",
"description": "Page type (og:type) (optional)\nYou should leave this blank for most of the time",
"default": "blog",
"nullable": true
},
"url": {
"type": "string",
"description": "Page URL (og:url)",
"description": "Page URL (og:url) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"image": {
@ -25,7 +25,7 @@
"string",
"array"
],
"description": "Page cover (og:image)",
"description": "Page cover (og:image) (optional) Default to the Open Graph image or thumbnail of the page\nYou should leave this blank for most of the time",
"items": {
"type": "string"
},
@ -33,12 +33,17 @@
},
"site_name": {
"type": "string",
"description": "Site name (og:site_name)",
"description": "Site name (og:site_name) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"author": {
"type": "string",
"description": "Page author (article:author) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"description": {
"type": "string",
"description": "Page description (og:description)",
"description": "Page description (og:description) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"twitter_card": {

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/misc/structured_data.json",
"description": "Structured data of the page\nhttps://developers.google.com/search/docs/guides/intro-structured-data",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Page title (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"description": {
"type": "string",
"description": "Page description (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"url": {
"type": "string",
"description": "Page URL (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"author": {
"type": "string",
"description": "Page author (article:author) (optional)\nYou should leave this blank for most of the time",
"nullable": true
},
"image": {
"type": [
"string",
"array"
],
"description": "Page images (optional) Default to the Open Graph image or thumbnail of the page\nYou should leave this blank for most of the time",
"items": {
"type": "string"
},
"nullable": true
}
},
"nullable": true
}

View File

@ -1,6 +1,7 @@
const { Component } = require('inferno');
const MetaTags = require('../misc/meta');
const OpenGraph = require('../misc/open_graph');
const StructuredData = require('../misc/structured_data');
const Plugins = require('./plugins');
function getPageTitle(page, siteTitle, helper) {
@ -39,7 +40,8 @@ module.exports = class extends Component {
} = config;
const {
meta = [],
open_graph,
open_graph = {},
structured_data = {},
canonical_url,
rss,
favicon
@ -75,7 +77,24 @@ module.exports = class extends Component {
let adsenseClientId = null;
if (Array.isArray(config.widgets)) {
adsenseClientId = config.widgets.find(widget => widget.type === 'adsense').client_id;
const widget = config.widgets.find(widget => widget.type === 'adsense');
if (widget) {
adsenseClientId = widget.client_id;
}
}
let openGraphImages = images;
if ((Array.isArray(open_graph.image) && open_graph.image.length > 0) || typeof open_graph.image === 'string') {
openGraphImages = open_graph.image;
} else if ((Array.isArray(page.photos) && page.photos.length > 0) || typeof page.photos === 'string') {
openGraphImages = page.photos;
}
let structuredImages = images;
if ((Array.isArray(structured_data.image) && structured_data.image.length > 0) || typeof structured_data.image === 'string') {
structuredImages = structured_data.image;
} else if ((Array.isArray(page.photos) && page.photos.length > 0) || typeof page.photos === 'string') {
structuredImages = page.photos;
}
return <head>
@ -86,16 +105,16 @@ module.exports = class extends Component {
<title>{getPageTitle(page, config.title, helper)}</title>
{open_graph ? <OpenGraph
{typeof open_graph === 'object' ? <OpenGraph
type={open_graph.type || (is_post(page) ? 'article' : 'website')}
title={open_graph.title || page.title || config.title}
date={page.date}
updated={page.updated}
author={config.author}
author={open_graph.author || config.author}
description={open_graph.description || page.description || page.excerpt || page.content || config.description}
keywords={page.keywords || (page.tags && page.tags.length ? page.tags : undefined) || config.keywords}
url={open_graph.url || url}
images={open_graph.image || page.photos || images}
images={openGraphImages}
siteName={open_graph.site_name || config.title}
language={language}
twitterId={open_graph.twitter_id}
@ -105,6 +124,15 @@ module.exports = class extends Component {
facebookAdmins={open_graph.fb_admins}
facebookAppId={open_graph.fb_app_id} /> : null}
{typeof structured_data === 'object' ? <StructuredData
title={structured_data.title || config.title}
description={structured_data.description || page.description || page.excerpt || page.content || config.description}
url={structured_data.url || page.permalink || url}
author={structured_data.author || config.author}
date={page.date}
updated={page.updated}
images={structuredImages} /> : null}
{canonical_url ? <link rel="canonical" href={canonical_url} /> : null}
{rss ? <link rel="alternative" href={url_for(rss)} title={config.title} type="application/atom+xml" /> : null}
{favicon ? <link rel="icon" href={url_for(favicon)} /> : null}

View File

@ -0,0 +1,56 @@
const urlFn = require('url');
const moment = require('moment');
const { Component } = require('inferno');
const { stripHTML, escapeHTML } = require('hexo-util');
module.exports = class extends Component {
render() {
const { title, url, author } = this.props;
let { description, images, date, updated } = this.props;
if (description) {
description = escapeHTML(stripHTML(description).substring(0, 200).trim())
.replace(/\n/g, ' ');
}
if (!Array.isArray(images)) {
images = [images];
}
images = images.map(path => {
if (!urlFn.parse(path).host) {
// resolve `path`'s absolute path relative to current page's url
// `path` can be both absolute (starts with `/`) or relative.
return urlFn.resolve(url, path);
}
return path;
}).filter(url => url.endsWith('.jpg') || url.endsWith('.png') || url.endsWith('.gif'));
if (date && (moment.isMoment(date) || moment.isDate(date)) && !isNaN(date.valueOf())) {
date = date.toISOString();
}
if (updated && (moment.isMoment(updated) || moment.isDate(updated)) && !isNaN(updated.valueOf())) {
updated = updated.toISOString();
}
const data = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
'mainEntityOfPage': {
'@type': 'WebPage',
'@id': url
},
'headline': title,
'image': images,
'datePublished': date,
'dateModified': updated,
'author': {
'@type': 'Person',
'name': author
},
'description': description
};
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}></script>;
}
};

View File

@ -37,5 +37,5 @@ module.exports = cacheComponent(AdSense, 'widget.adsense', props => {
title: helper.__('widget.adsense'),
clientId: client_id,
slotId: slot_id
}
});
};
});