【Hexo】自訂 Icarus 主題的 JSON-LD 優化 SEO

【Hexo】自訂 Icarus 主題的 JSON-LD 優化 SEO

本篇重點

  • 覆寫 Icarus 預設 Structured Data,改為自訂元件控制
  • 建立 structured_data.jsx,依頁面類型動態產生不同的 JSON-LD,提升 SEO 表現

Icarus 主題原生提供 head.structured_data 設定,可以在 _config.icarus.yml 定義基本欄位(如 title、description 等),並自動產生對應的 JSON-LD。但屬於靜態配置,生成內容較為簡化,導致結構資訊不夠完整,無法充分發揮 Structured Data 在搜尋引擎解析的效果。

因此決定將 Structured Data 改為由自訂元件統一生成,透過程式邏輯依頁面類型動態輸出對應的 JSON-LD(如 WebSite、TechArticle、CollectionPage、BreadcrumbList 等),並讓 JSON-LD 結構更完整且具擴展性,同時強化搜尋引擎對內容的理解,進一步提升整體 SEO 表現。

目標

透過修改 Hexo Icarus 主題原始碼,覆蓋預設的 SEO 輸出,建立並套用專屬於個人網站的 JSON-LD 結構化資料。

覆寫 Icarus 預設 Structured Data

Icarus 預設使用 hexo-component-inferno 提供的 Structured Data,替換掉此檔案的引用路徑,改為引入自定義的結構化資料元件,並修改元件 props 以利後續新增的 structured_data.jsx

修改內容

layout/common/head.jsx >folded查看檔案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...

- const StructuredData = require('hexo-component-inferno/lib/view/misc/structured_data');
+ const StructuredData = require('../misc/structured_data');

...

{typeof structured_data === 'object' && structured_data !== null ? <StructuredData
- title={structured_data.title || page.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}
- publisher={structured_data.publisher || config.title}
- publisherLogo={structured_data.publisher_logo || config.logo}
- date={page.date}
- updated={page.updated}
+ page={page}
+ config={config}
+ helper={helper}
images={structuredImages} /> : null}

...
  • 移除原本套件輸出,導入自定義 structured_data.jsx
  • 修改元件 props

建立自定義 Structured Data 元件

在主題資料夾內新增 layout/misc/structured_data.jsx,此檔案將覆蓋 Icarus 預設的 JSON-LD 生成邏輯,內容可依個人需求對結構化資料做調整。

產出結構

透過 Hexo 的 Helper 函式(如 is_home(), is_post(), is_category()),根據當前頁面類型,動態產出不同的結構化資料。

  • 首頁:輸出 WebSite 結構。
  • 文章頁:輸出 TechArticleBreadcrumbList 結構。
  • 分類頁:輸出 CollectionPageBreadcrumbList 結構。
  • 關於頁:輸出 AboutPagePerson 結構。
  • 其他頁面:輸出基礎 WebPage 結構。

替換自定義內容

將所有含有 example 的相關網址,全面替換為個人專屬的網站資訊。

新增內容

layout/misc/structured_data.jsx >folded查看檔案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
* A JSX component that renders simple Google structured data.
* @module layout/misc/structured_data
*/
const { Component } = require('inferno');
const { stripHTML, escapeHTML } = require('hexo-util');

/**
* A JSX component that renders simple Google structured data.
*
* @name StructuredData
*/
module.exports = class extends Component {
render() {
const { page, config, helper, images } = this.props;
const { full_url_for, is_home, is_post, is_category, is_page } = helper;
const { structured_data = {} } = config.head;
const siteUrl = config.url;
const language = config.language;
const title = structured_data.title || page.title || config.title;
let description = structured_data.description || page.description || page.excerpt || page.content || config.description;
const canonical = full_url_for(structured_data.url || page.permalink || page.current_url || config.url);
const author = structured_data.author || config.author;
const publisher = structured_data.publisher || title;
const publisherLogo = structured_data.publisher_logo || config.logo;
const schemaData = [];

const person = {
'@type': 'Person',
'@id': 'https://example.github.io/about/#person',
name: author,
image: 'https://example.github.io/img/og_image.png',
url: 'https://example.github.io/about/',
jobTitle: "Software Engineer",
sameAs: [
"https://github.com/example",
]
};

const organization = {
'@type': 'Organization',
'@id': `${siteUrl}#organization`,
name: publisher,
url: siteUrl,
logo: {
'@type': 'ImageObject',
url: full_url_for(publisherLogo)
}
};

const breadcrumbList = {
'@type': 'BreadcrumbList',
'@id': `${canonical}#breadcrumb`,
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: '分類',
item: 'https://example.github.io/categories/'
}
]
};

if (description) {
description = escapeHTML(
stripHTML(description)
.replace(/\n/g, ' ')
.slice(0, 210)
.trim()
);
}

// 首頁(WebSite)
if (is_home()) {
schemaData.push({
'@type': 'WebSite',
'@id': `${siteUrl}#website`,
url: siteUrl,
name: title,
alternateName: "example",
inLanguage: language,
author: person,
publisher: organization,
description: description,
});
}

// 文章頁 (TechArticle + BreadcrumbList)
if (is_post()) {
schemaData.push({
'@type': 'TechArticle',
'@id': `${canonical}#article`,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonical
},
url: canonical,
headline: title,
description: description,
image: [full_url_for(images)],
datePublished: page.date ? page.date.toISOString() : undefined,
dateModified: page.updated ? page.updated.toISOString() : undefined,
inLanguage: language,
author: person,
publisher: organization,
keywords: page.tags ? page.tags.map(t => t.name).join(',') : undefined,
articleSection: page.categories ? page.categories.map(cat => cat.name) : undefined
});

let position = 2;
if (page.categories && page.categories.length > 0) {
page.categories.forEach((cat) => {
breadcrumbList.itemListElement.push({
'@type': 'ListItem',
position: position++,
name: cat.name,
item: full_url_for(cat.path)
});
});
}

breadcrumbList.itemListElement.push({
'@type': 'ListItem',
position: position,
name: title,
item: canonical
});

schemaData.push(breadcrumbList);
}

// 分類頁 (CollectionPage)
if (is_category()) {
const categoryName = page.category || title;

schemaData.push({
'@type': 'CollectionPage',
'@id': `${canonical}#collection`,
name: categoryName + ' 技術文章列表',
headline: categoryName + ' 技術文章列表',
url: canonical,
description: `收錄 ${categoryName} 相關技術文章、經驗分享整理。`,
inLanguage: language,
mainEntity: {
'@type': 'ItemList',
'@id': `${canonical}#itemlist`,
numberOfItems: page.posts.length,
itemListElement: (page.posts).slice(0, 20).map((post, index) => ({
'@type': 'ListItem',
position: index + 1,
url: full_url_for(post.link || post.path),
name: post.title
}))
}
});

let position = 2;

if (page.parents && page.parents.length > 0) {
page.parents.forEach((cat) => {
breadcrumbList.itemListElement.push({
'@type': 'ListItem',
position: position++,
name: cat.name,
item: full_url_for(cat.path)
});
});
}
breadcrumbList.itemListElement.push({
'@type': 'ListItem',
position: position++,
name: page.category,
item: canonical
});
schemaData.push(breadcrumbList);
}

if (!is_home() && !is_post() && !is_category()) {
if (is_page() && page.path.startsWith('about/')) {
schemaData.push({
'@type': 'AboutPage',
'@id': `${canonical}#aboutpage`,
url: canonical,
name: '關於 ' + author,
headline: '關於 ' + author,
description: description,
inLanguage: language,
mainEntity: {
"@id": "https://example.github.io/about/#person"
}
});
schemaData.push(person);
} else {
schemaData.push({
'@type': 'WebPage',
'@id': `${canonical}#webpage`,
url: canonical,
name: title,
headline: title,
description: description,
inLanguage: language,
author: person,
publisher: organization,
});
}
}

if (schemaData.length === 0) return null;

const finalSchema = {
'@context': 'https://schema.org',
'@graph': schemaData
};

return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(finalSchema) }}
></script>
);
}
};

延伸閱讀

發表於

2026-04-20

更新於

2026-04-20

許可協議


你可能也想看

【Hexo】Icarus 主題串接 GA 顯示網站 PV 和 UV
【GitHub、Hexo】jsDelivr CDN 加速 GitHub Pages 資源及 Hexo 實作
【Git、Hexo】deploy github 檔名大小寫問題

評論

複製完成