feat(layout): add structured data support
parent
ea66ff939d
commit
48fd263ea5
|
@ -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);
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
"open_graph": {
|
||||
"$ref": "/misc/open_graph.json"
|
||||
},
|
||||
"structured_data": {
|
||||
"$ref": "/misc/structured_data.json"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "/misc/meta.json"
|
||||
},
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 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}
|
||||
|
|
|
@ -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'),
|
||||
clientId: client_id,
|
||||
slotId: slot_id
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue