feat(layout): add structured data support
This commit is contained in:
parent
ea66ff939d
commit
48fd263ea5
|
@ -18,7 +18,7 @@ module.exports = hexo => {
|
||||||
const ALTERNATIVE_CONFIG = {
|
const ALTERNATIVE_CONFIG = {
|
||||||
post: getThemeConfig('.post'),
|
post: getThemeConfig('.post'),
|
||||||
page: getThemeConfig('.page')
|
page: getThemeConfig('.page')
|
||||||
}
|
};
|
||||||
|
|
||||||
function getExtraConfig(source, reservedKeys) {
|
function getExtraConfig(source, reservedKeys) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
@ -48,7 +48,7 @@ module.exports = hexo => {
|
||||||
if (page) {
|
if (page) {
|
||||||
if ((page.layout !== 'page' || page.layout !== 'post') && ALTERNATIVE_CONFIG[page.layout]) {
|
if ((page.layout !== 'page' || page.layout !== 'post') && ALTERNATIVE_CONFIG[page.layout]) {
|
||||||
// load alternative config if exists
|
// 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 {
|
} else {
|
||||||
// site config already merged into theme config in hexo/lib/hexo/index.js#Hexo.prototype._generateLocals()
|
// site config already merged into theme config in hexo/lib/hexo/index.js#Hexo.prototype._generateLocals()
|
||||||
locals.config = Object.assign({}, Object.getPrototypeOf(locals).theme);
|
locals.config = Object.assign({}, Object.getPrototypeOf(locals).theme);
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
"open_graph": {
|
"open_graph": {
|
||||||
"$ref": "/misc/open_graph.json"
|
"$ref": "/misc/open_graph.json"
|
||||||
},
|
},
|
||||||
|
"structured_data": {
|
||||||
|
"$ref": "/misc/structured_data.json"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "/misc/meta.json"
|
"$ref": "/misc/meta.json"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,18 +6,18 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string",
|
"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
|
"nullable": true
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"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",
|
"default": "blog",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string",
|
"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
|
"nullable": true
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
"string",
|
"string",
|
||||||
"array"
|
"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": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -33,12 +33,17 @@
|
||||||
},
|
},
|
||||||
"site_name": {
|
"site_name": {
|
||||||
"type": "string",
|
"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
|
"nullable": true
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"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
|
"nullable": true
|
||||||
},
|
},
|
||||||
"twitter_card": {
|
"twitter_card": {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
const { Component } = require('inferno');
|
const { Component } = require('inferno');
|
||||||
const MetaTags = require('../misc/meta');
|
const MetaTags = require('../misc/meta');
|
||||||
const OpenGraph = require('../misc/open_graph');
|
const OpenGraph = require('../misc/open_graph');
|
||||||
|
const StructuredData = require('../misc/structured_data');
|
||||||
const Plugins = require('./plugins');
|
const Plugins = require('./plugins');
|
||||||
|
|
||||||
function getPageTitle(page, siteTitle, helper) {
|
function getPageTitle(page, siteTitle, helper) {
|
||||||
|
@ -39,7 +40,8 @@ module.exports = class extends Component {
|
||||||
} = config;
|
} = config;
|
||||||
const {
|
const {
|
||||||
meta = [],
|
meta = [],
|
||||||
open_graph,
|
open_graph = {},
|
||||||
|
structured_data = {},
|
||||||
canonical_url,
|
canonical_url,
|
||||||
rss,
|
rss,
|
||||||
favicon
|
favicon
|
||||||
|
@ -75,7 +77,24 @@ module.exports = class extends Component {
|
||||||
|
|
||||||
let adsenseClientId = null;
|
let adsenseClientId = null;
|
||||||
if (Array.isArray(config.widgets)) {
|
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>
|
return <head>
|
||||||
|
@ -86,16 +105,16 @@ module.exports = class extends Component {
|
||||||
|
|
||||||
<title>{getPageTitle(page, config.title, helper)}</title>
|
<title>{getPageTitle(page, config.title, helper)}</title>
|
||||||
|
|
||||||
{open_graph ? <OpenGraph
|
{typeof open_graph === 'object' ? <OpenGraph
|
||||||
type={open_graph.type || (is_post(page) ? 'article' : 'website')}
|
type={open_graph.type || (is_post(page) ? 'article' : 'website')}
|
||||||
title={open_graph.title || page.title || config.title}
|
title={open_graph.title || page.title || config.title}
|
||||||
date={page.date}
|
date={page.date}
|
||||||
updated={page.updated}
|
updated={page.updated}
|
||||||
author={config.author}
|
author={open_graph.author || config.author}
|
||||||
description={open_graph.description || page.description || page.excerpt || page.content || config.description}
|
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}
|
keywords={page.keywords || (page.tags && page.tags.length ? page.tags : undefined) || config.keywords}
|
||||||
url={open_graph.url || url}
|
url={open_graph.url || url}
|
||||||
images={open_graph.image || page.photos || images}
|
images={openGraphImages}
|
||||||
siteName={open_graph.site_name || config.title}
|
siteName={open_graph.site_name || config.title}
|
||||||
language={language}
|
language={language}
|
||||||
twitterId={open_graph.twitter_id}
|
twitterId={open_graph.twitter_id}
|
||||||
|
@ -105,6 +124,15 @@ module.exports = class extends Component {
|
||||||
facebookAdmins={open_graph.fb_admins}
|
facebookAdmins={open_graph.fb_admins}
|
||||||
facebookAppId={open_graph.fb_app_id} /> : null}
|
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}
|
{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}
|
{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}
|
{favicon ? <link rel="icon" href={url_for(favicon)} /> : null}
|
||||||
|
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
};
|
|
@ -37,5 +37,5 @@ module.exports = cacheComponent(AdSense, 'widget.adsense', props => {
|
||||||
title: helper.__('widget.adsense'),
|
title: helper.__('widget.adsense'),
|
||||||
clientId: client_id,
|
clientId: client_id,
|
||||||
slotId: slot_id
|
slotId: slot_id
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue