hexo-theme-gnix 9.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +4 -2
  2. package/include/hexo/feed.js +5 -5
  3. package/include/hexo/filter.js +25 -1
  4. package/include/hexo/generator/archive.js +116 -0
  5. package/include/hexo/generator/home.js +64 -0
  6. package/include/hexo/generator/index.js +82 -0
  7. package/include/hexo/generator/md_generator.js +87 -0
  8. package/include/hexo/generator/page.js +55 -0
  9. package/include/hexo/generator/tag.js +84 -0
  10. package/include/hexo/helper.js +38 -0
  11. package/include/hexo/i18n.js +183 -0
  12. package/include/util/article_font.js +132 -0
  13. package/include/util/i18n.js +280 -0
  14. package/include/util/theme.js +84 -0
  15. package/languages/en.yml +28 -0
  16. package/languages/zh-CN.yml +28 -0
  17. package/layout/archive.jsx +131 -127
  18. package/layout/common/article.jsx +283 -16
  19. package/layout/common/article_info.jsx +339 -0
  20. package/layout/common/article_media.jsx +11 -4
  21. package/layout/common/comment.jsx +15 -7
  22. package/layout/common/footer.jsx +6 -5
  23. package/layout/common/head.jsx +121 -32
  24. package/layout/common/navbar.jsx +195 -65
  25. package/layout/common/theme_selector.jsx +16 -14
  26. package/layout/layout.jsx +43 -5
  27. package/layout/misc/open_graph.jsx +162 -66
  28. package/layout/misc/paginator.jsx +2 -8
  29. package/layout/plugin/cookie_consent.jsx +252 -53
  30. package/layout/plugin/swup.jsx +1 -1
  31. package/layout/search/insight.jsx +1 -1
  32. package/layout/tag.jsx +3 -2
  33. package/layout/tags.jsx +81 -73
  34. package/package.json +5 -5
  35. package/scripts/index.js +1 -0
  36. package/source/css/archive.css +225 -180
  37. package/source/css/default.css +1162 -98
  38. package/source/css/responsive.css +426 -0
  39. package/source/css/shiki/shiki.css +12 -2081
  40. package/source/css/tags.css +183 -0
  41. package/source/css/twikoo.css +1049 -1045
  42. package/source/img/favicon.svg +1 -6
  43. package/source/img/og_image.webp +0 -0
  44. package/source/js/article-font-utils.js +99 -0
  45. package/source/js/busuanzi.js +91 -24
  46. package/source/js/components/chat.js +169 -50
  47. package/source/js/components/image-carousel.js +152 -108
  48. package/source/js/components/sidenote.js +210 -0
  49. package/source/js/components/text-image-section.js +78 -90
  50. package/source/js/components/theme-stacked.js +65 -33
  51. package/source/js/components/tree.js +30 -16
  52. package/source/js/decrypt.js +7 -2
  53. package/source/js/main.js +428 -5
  54. package/source/js/swup.js +39 -0
  55. package/source/js/theme-selector.js +26 -16
  56. package/include/hexo/generator.js +0 -53
  57. package/layout/misc/article_licensing.jsx +0 -99
  58. package/source/css/responsive/desktop.css +0 -36
  59. package/source/css/responsive/mobile.css +0 -29
  60. package/source/css/responsive/tablet.css +0 -43
  61. package/source/css/responsive/touch.css +0 -155
  62. package/source/img/logo.svg +0 -9
  63. package/source/js/archive-breadcrumb.js +0 -132
  64. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +0 -6
  65. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +0 -1
  66. package/source/js/swup.bundle.js +0 -1
@@ -0,0 +1,339 @@
1
+ const { Component } = require("../../include/util/common");
2
+
3
+ function getTranslatedValue(helper, key, fallback) {
4
+ const translated = helper.__(key);
5
+ return translated === key ? fallback : translated;
6
+ }
7
+
8
+ function getTranslationMethodLabel(method, helper) {
9
+ if (!method) return "";
10
+ const key = `article.translation_methods.${method}`;
11
+ return getTranslatedValue(helper, key, method);
12
+ }
13
+
14
+ function getOriginalWorkValue(page, helper) {
15
+ const value = page.i18n?.original ?? page.original;
16
+ if (typeof value !== "boolean") return null;
17
+ return value ? helper.__("article.yes") : helper.__("article.no");
18
+ }
19
+
20
+ function getTranslationNote(page, helper) {
21
+ const translation = page.i18n?.translation || page.translation;
22
+ if (!translation) return null;
23
+ if (typeof translation === "string") return translation;
24
+
25
+ const parts = [];
26
+ const method = getTranslationMethodLabel(translation.method, helper);
27
+ if (method) parts.push(method);
28
+ if (typeof translation.reviewed === "boolean") {
29
+ parts.push(helper.__(translation.reviewed ? "article.translation_reviewed" : "article.translation_not_reviewed"));
30
+ }
31
+ if (translation.note) parts.push(translation.note);
32
+
33
+ return parts.length ? parts.join(" · ") : null;
34
+ }
35
+
36
+ function LanguageIcon({ title }) {
37
+ return (
38
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label={title}>
39
+ <title>{title}</title>
40
+ <path d="m5 8 6 6" />
41
+ <path d="m4 14 6-6 2-3" />
42
+ <path d="M2 5h12" />
43
+ <path d="M7 2h1" />
44
+ <path d="m22 22-5-10-5 10" />
45
+ <path d="M14 18h6" />
46
+ </svg>
47
+ );
48
+ }
49
+
50
+ module.exports = class extends Component {
51
+ render() {
52
+ const { page, config, helper } = this.props;
53
+ const { article } = config;
54
+ const originalWorkValue = getOriginalWorkValue(page, helper);
55
+ const translationNote = getTranslationNote(page, helper);
56
+
57
+ const markdownSourceUrl = page.markdown_path ? helper.url_for(page.markdown_path) : null;
58
+ const markdownSourceLabel = helper.__("article.markdown_source");
59
+ const markdownSourceType = "text/markdown; charset=utf-8";
60
+
61
+ return (
62
+ <div id="article-info-popover" popover="auto" class="article-popover article-info-popover">
63
+ <div class="article-popover-header">
64
+ <h3>{helper.__("article.article_info")}</h3>
65
+ <button type="button" class="article-popover-close" popovertarget="article-info-popover" popovertargetaction="hide" aria-label={helper.__("article.close")}>
66
+ <svg
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ width="18"
69
+ height="18"
70
+ viewBox="0 0 24 24"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ role="img"
77
+ aria-label="Close"
78
+ >
79
+ <title>Close</title>
80
+ <path d="M18 6 6 18" />
81
+ <path d="m6 6 12 12" />
82
+ </svg>
83
+ </button>
84
+ </div>
85
+ <div class="article-popover-body">
86
+ <div class="article-info-list">
87
+ {(page.author || config.author) && (
88
+ <div class="article-info-item">
89
+ <div class="article-info-icon">
90
+ <svg
91
+ xmlns="http://www.w3.org/2000/svg"
92
+ width="18"
93
+ height="18"
94
+ viewBox="0 0 24 24"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ stroke-width="2"
98
+ stroke-linecap="round"
99
+ stroke-linejoin="round"
100
+ role="img"
101
+ aria-label="Author"
102
+ >
103
+ <title>Author</title>
104
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
105
+ <circle cx="12" cy="7" r="4" />
106
+ </svg>
107
+ </div>
108
+ <div class="article-info-content">
109
+ <span class="article-info-label">{helper.__("article.author")}</span>
110
+ <span class="article-info-value">{page.author || config.author}</span>
111
+ </div>
112
+ </div>
113
+ )}
114
+ {page.title && (
115
+ <div class="article-info-item">
116
+ <div class="article-info-icon">
117
+ <svg
118
+ xmlns="http://www.w3.org/2000/svg"
119
+ width="18"
120
+ height="18"
121
+ viewBox="0 0 24 24"
122
+ fill="none"
123
+ stroke="currentColor"
124
+ stroke-width="2"
125
+ stroke-linecap="round"
126
+ stroke-linejoin="round"
127
+ role="img"
128
+ aria-label="Title"
129
+ >
130
+ <title>Title</title>
131
+ <path d="M4 20h16" />
132
+ <path d="M6 16h6" />
133
+ <path d="M6 12h12" />
134
+ <path d="M6 8h10" />
135
+ </svg>
136
+ </div>
137
+ <div class="article-info-content">
138
+ <span class="article-info-label">{helper.__("article.article_title")}</span>
139
+ <span class="article-info-value">{page.title}</span>
140
+ </div>
141
+ </div>
142
+ )}
143
+ {page.permalink && (
144
+ <div class="article-info-item">
145
+ <div class="article-info-icon">
146
+ <svg
147
+ xmlns="http://www.w3.org/2000/svg"
148
+ width="18"
149
+ height="18"
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ stroke-width="2"
154
+ stroke-linecap="round"
155
+ stroke-linejoin="round"
156
+ role="img"
157
+ aria-label="URL"
158
+ >
159
+ <title>URL</title>
160
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
161
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
162
+ </svg>
163
+ </div>
164
+ <div class="article-info-content">
165
+ <span class="article-info-label">{helper.__("article.url")}</span>
166
+ <span class="article-info-value">
167
+ <a href={page.permalink} target="_blank" rel="noopener">
168
+ {decodeURI(page.permalink)}
169
+ </a>
170
+ </span>
171
+ </div>
172
+ </div>
173
+ )}
174
+ {originalWorkValue && (
175
+ <div class="article-info-item">
176
+ <div class="article-info-icon">
177
+ <LanguageIcon title={helper.__("article.original_work")} />
178
+ </div>
179
+ <div class="article-info-content">
180
+ <span class="article-info-label">{helper.__("article.original_work")}</span>
181
+ <span class="article-info-value">{originalWorkValue}</span>
182
+ </div>
183
+ </div>
184
+ )}
185
+ {translationNote && (
186
+ <div class="article-info-item">
187
+ <div class="article-info-icon">
188
+ <LanguageIcon title={helper.__("article.translation_note")} />
189
+ </div>
190
+ <div class="article-info-content">
191
+ <span class="article-info-label">{helper.__("article.translation_note")}</span>
192
+ <span class="article-info-value">{translationNote}</span>
193
+ </div>
194
+ </div>
195
+ )}
196
+ {markdownSourceUrl && (
197
+ <div class="article-info-item">
198
+ <div class="article-info-icon">
199
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 640 512" role="img" aria-label={markdownSourceLabel}>
200
+ <title>{markdownSourceLabel}</title>
201
+ <path
202
+ fill="currentColor"
203
+ d="M593.8 59.1H46.2C20.7 59.1 0 79.8 0 105.2v301.5c0 25.5 20.7 46.2 46.2 46.2h547.7c25.5 0 46.2-20.7 46.1-46.1V105.2c0-25.4-20.7-46.1-46.2-46.1M338.5 360.6H277v-120l-61.5 76.9l-61.5-76.9v120H92.3V151.4h61.5l61.5 76.9l61.5-76.9h61.5v209.2zm135.3 3.1L381.5 256H443V151.4h61.5V256H566z"
204
+ />
205
+ </svg>
206
+ </div>
207
+ <div class="article-info-content">
208
+ <span class="article-info-label">{markdownSourceLabel}</span>
209
+ <span class="article-info-value">
210
+ <a href={markdownSourceUrl} target="_blank" rel="noopener" type={markdownSourceType}>
211
+ {decodeURI(markdownSourceUrl)}
212
+ </a>
213
+ </span>
214
+ </div>
215
+ </div>
216
+ )}
217
+ {page.date && (
218
+ <div class="article-info-item">
219
+ <div class="article-info-icon">
220
+ <svg
221
+ xmlns="http://www.w3.org/2000/svg"
222
+ width="18"
223
+ height="18"
224
+ viewBox="0 0 24 24"
225
+ fill="none"
226
+ stroke="currentColor"
227
+ stroke-width="2"
228
+ stroke-linecap="round"
229
+ stroke-linejoin="round"
230
+ role="img"
231
+ aria-label="Created time"
232
+ >
233
+ <title>Created time</title>
234
+ <circle cx="12" cy="12" r="10" />
235
+ <polyline points="12 6 12 12 16 14" />
236
+ </svg>
237
+ </div>
238
+ <div class="article-info-content">
239
+ <span class="article-info-label">{helper.__("article.created_time")}</span>
240
+ <span class="article-info-value">{helper.date(page.date, "YYYY-MM-DD HH:mm")}</span>
241
+ </div>
242
+ </div>
243
+ )}
244
+ {page.updated && (
245
+ <div class="article-info-item">
246
+ <div class="article-info-icon">
247
+ <svg
248
+ xmlns="http://www.w3.org/2000/svg"
249
+ width="18"
250
+ height="18"
251
+ viewBox="0 0 24 24"
252
+ fill="none"
253
+ stroke="currentColor"
254
+ stroke-width="2"
255
+ stroke-linecap="round"
256
+ stroke-linejoin="round"
257
+ role="img"
258
+ aria-label="Updated time"
259
+ >
260
+ <title>Updated time</title>
261
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
262
+ <path d="M3 3v5h5" />
263
+ </svg>
264
+ </div>
265
+ <div class="article-info-content">
266
+ <span class="article-info-label">{helper.__("article.updated_time")}</span>
267
+ <span class="article-info-value">{helper.date(page.updated, "YYYY-MM-DD HH:mm")}</span>
268
+ </div>
269
+ </div>
270
+ )}
271
+ {article?.licenses && Object.keys(article.licenses).length > 0 && (
272
+ <div class="article-info-item">
273
+ <div class="article-info-icon">
274
+ <svg
275
+ xmlns="http://www.w3.org/2000/svg"
276
+ width="18"
277
+ height="18"
278
+ viewBox="0 0 24 24"
279
+ fill="none"
280
+ stroke="currentColor"
281
+ stroke-width="2"
282
+ stroke-linecap="round"
283
+ stroke-linejoin="round"
284
+ role="img"
285
+ aria-label="License"
286
+ >
287
+ <title>License</title>
288
+ <path fill="none" d="M15 21H6a3 3 0 0 1-3-3v-1h10v2a2 2 0 0 0 4 0V5a2 2 0 1 1 2 2h-2m2-4H8a3 3 0 0 0-3 3v11M9 7h4m-4 4h4" />
289
+ <polyline points="14 2 14 8 20 8" />
290
+ </svg>
291
+ </div>
292
+ <div class="article-info-content">
293
+ <span class="article-info-label">{helper.__("article.license")}</span>
294
+ <span class="article-info-value">
295
+ {Object.keys(article.licenses).map((name, i) => (
296
+ <span key={name}>
297
+ {i > 0 && <span>, </span>}
298
+ <a href={article.licenses[name]} target="_blank" rel="noopener">
299
+ {name}
300
+ </a>
301
+ </span>
302
+ ))}
303
+ </span>
304
+ </div>
305
+ </div>
306
+ )}
307
+ {page.location && (
308
+ <div class="article-info-item">
309
+ <div class="article-info-icon">
310
+ <svg
311
+ xmlns="http://www.w3.org/2000/svg"
312
+ width="18"
313
+ height="18"
314
+ viewBox="0 0 24 24"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ stroke-width="2"
318
+ stroke-linecap="round"
319
+ stroke-linejoin="round"
320
+ role="img"
321
+ aria-label="Location"
322
+ >
323
+ <title>Location</title>
324
+ <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
325
+ <circle cx="12" cy="10" r="3" />
326
+ </svg>
327
+ </div>
328
+ <div class="article-info-content">
329
+ <span class="article-info-label">{helper.__("article.location")}</span>
330
+ <span class="article-info-value">{page.location}</span>
331
+ </div>
332
+ </div>
333
+ )}
334
+ </div>
335
+ </div>
336
+ </div>
337
+ );
338
+ }
339
+ };
@@ -1,18 +1,25 @@
1
1
  /**
2
2
  * Article media component, used in article lists such as archive page and recent posts widget
3
3
  */
4
- const { Component, dateFormatters, parseISO } = require("../../include/util/common");
4
+ const { Component, dateFormatters, isValidDate, parseISO } = require("../../include/util/common");
5
+
6
+ function formatDate(date, dateXml) {
7
+ if (date) return date;
8
+
9
+ const parsedDate = parseISO(dateXml);
10
+ return isValidDate(parsedDate) ? dateFormatters.shortDay.format(parsedDate) : "";
11
+ }
5
12
 
6
13
  module.exports = class extends Component {
7
14
  render() {
8
- const { url, title, date } = this.props;
9
- const formattedDate = dateFormatters.shortDay.format(parseISO(date));
15
+ const { url, title, date, dateXml } = this.props;
16
+ const formattedDate = formatDate(date, dateXml);
10
17
 
11
18
  return (
12
19
  <article class="archive-item">
13
20
  <div>
14
21
  <p class="article-meta">
15
- <span>{formattedDate}</span>
22
+ <time dateTime={dateXml || null}>{formattedDate}</time>
16
23
  </p>
17
24
  <a class="archive-title" href={url}>
18
25
  {title}
@@ -2,20 +2,28 @@ const { Component, loadComponent } = require("../../include/util/common");
2
2
 
3
3
  module.exports = class extends Component {
4
4
  render() {
5
- const { config, page, helper } = this.props;
5
+ const { config, page, helper, embedded = false } = this.props;
6
6
  const { comment } = config;
7
7
  if (!comment || typeof comment.type !== "string") {
8
8
  return null;
9
9
  }
10
10
 
11
+ const content = (() => {
12
+ const Comment = loadComponent(`comment/${comment.type}`);
13
+ return <Comment config={config} page={page} helper={helper} comment={comment} />;
14
+ })();
15
+
16
+ if (embedded) {
17
+ return (
18
+ <div class="article-comment-panel" id="comments">
19
+ {content}
20
+ </div>
21
+ );
22
+ }
23
+
11
24
  return (
12
25
  <div class="card" id="comments">
13
- <div class="card-content">
14
- {(() => {
15
- const Comment = loadComponent(`comment/${comment.type}`);
16
- return <Comment config={config} page={page} helper={helper} comment={comment} />;
17
- })()}
18
- </div>
26
+ <div class="card-content">{content}</div>
19
27
  </div>
20
28
  );
21
29
  }
@@ -66,7 +66,7 @@ class Footer extends Component {
66
66
 
67
67
  const footer_subdomains = (
68
68
  <div class="footer-column footer-subdomains">
69
- <p class="footer-heading">Sub Domains</p>
69
+ <p class="footer-heading">Quick Links</p>
70
70
  <div class="footer-links">
71
71
  {Object.keys(subdomains).length
72
72
  ? Object.keys(subdomains).map((name) => {
@@ -95,9 +95,10 @@ class Footer extends Component {
95
95
  }
96
96
 
97
97
  module.exports = cacheComponent(Footer, "common.footer", (props) => {
98
- const { config, helper, site } = props;
98
+ const { config, helper, page, site } = props;
99
99
  const { url_for, _p, date } = helper;
100
100
  const { title, author, footer, plugins } = config;
101
+ const langKey = helper.language_key(page);
101
102
 
102
103
  const links = {};
103
104
  if (footer?.links) {
@@ -116,7 +117,7 @@ module.exports = cacheComponent(Footer, "common.footer", (props) => {
116
117
  const link = footer.subdomains[name];
117
118
  const targetUrl = typeof link === "string" ? link : link.url;
118
119
  subdomains[name] = {
119
- url: url_for(targetUrl),
120
+ url: helper.localized_url_for(targetUrl, langKey),
120
121
  };
121
122
  });
122
123
  }
@@ -139,11 +140,11 @@ module.exports = cacheComponent(Footer, "common.footer", (props) => {
139
140
 
140
141
  archives = Object.keys(byYear)
141
142
  .sort((a, b) => Number(b) - Number(a))
142
- .map((year) => ({ year, url: url_for(`${archiveDir}/${year}/`) }));
143
+ .map((year) => ({ year, url: helper.localized_url_for(`${archiveDir}/${year}/`, langKey) }));
143
144
  }
144
145
 
145
146
  return {
146
- siteUrl: url_for("/"),
147
+ siteUrl: helper.localized_url_for("/", langKey),
147
148
  siteTitle: title,
148
149
  siteYear: date(new Date(), "YYYY"),
149
150
  author,
@@ -3,6 +3,11 @@ const MetaTags = require("../../layout/misc/meta");
3
3
  const OpenGraph = require("../../layout/misc/open_graph");
4
4
  const StructuredData = require("../../layout/misc/structured_data");
5
5
  const Plugins = require("./plugins");
6
+ const { getArticleFontInitScript } = require("../../include/util/article_font");
7
+ const { getThemeInitScript } = require("../../include/util/theme");
8
+ const { getDefaultLanguageKey, getI18nKey, getLanguage, getPageLanguageKey, getPageLocale, isI18nEnabled, normalizeLocale, toArray } = require("../../include/util/i18n");
9
+ const fs = require("node:fs");
10
+ const path = require("node:path");
6
11
 
7
12
  function getPageTitle(page, siteTitle, helper) {
8
13
  let title = page.title;
@@ -23,16 +28,111 @@ function getPageTitle(page, siteTitle, helper) {
23
28
  return [title, siteTitle].filter((str) => typeof str !== "undefined" && str.trim() !== "").join(" - ");
24
29
  }
25
30
 
31
+ function getTermName(term) {
32
+ if (!term) return undefined;
33
+ if (typeof term === "string") return term;
34
+
35
+ return term.name || term.slug || term.path;
36
+ }
37
+
38
+ function getArticleSection(page) {
39
+ if (page.category) return getTermName(page.category);
40
+
41
+ const categories = page.categories;
42
+ if (!categories) return undefined;
43
+ if (typeof categories.first === "function") return getTermName(categories.first());
44
+ if (Array.isArray(categories.data)) return getTermName(categories.data[0]);
45
+ if (Array.isArray(categories)) return getTermName(categories[0]);
46
+
47
+ return undefined;
48
+ }
49
+
50
+ function toAbsoluteUrl(href, helper, config) {
51
+ if (!href || typeof href !== "string") return null;
52
+ if (/^https?:\/\//i.test(href)) return href;
53
+ if (typeof helper.full_url_for === "function") return helper.full_url_for(href);
54
+
55
+ const localUrl = typeof helper.url_for === "function" ? helper.url_for(href) : href;
56
+ if (/^https?:\/\//i.test(localUrl)) return localUrl;
57
+
58
+ const siteUrl = String(config.url || "").replace(/\/+$/, "");
59
+ const path = localUrl.startsWith("/") ? localUrl : `/${localUrl}`;
60
+ return `${siteUrl}${path}`;
61
+ }
62
+
63
+ function addAlternateLink(links, hreflang, href, helper, config) {
64
+ const normalizedHreflang = hreflang === "x-default" ? "x-default" : normalizeLocale(hreflang);
65
+ const absoluteHref = toAbsoluteUrl(href, helper, config);
66
+ if (!normalizedHreflang || !absoluteHref) return;
67
+ links.set(normalizedHreflang.toLowerCase(), {
68
+ hreflang: normalizedHreflang,
69
+ href: absoluteHref,
70
+ });
71
+ }
72
+
73
+ function addExplicitAlternates(links, alternates, helper, config) {
74
+ if (!alternates || typeof alternates !== "object") return;
75
+
76
+ Object.keys(alternates).forEach((key) => {
77
+ const value = alternates[key];
78
+ const href = typeof value === "string" ? value : value?.href || value?.url;
79
+ const language = getLanguage(config, key);
80
+ const hreflang = key === "x-default" ? "x-default" : language?.key === key ? language.locale : key;
81
+ addAlternateLink(links, hreflang, href, helper, config);
82
+ });
83
+ }
84
+
85
+ function collectDocuments(site) {
86
+ return [...toArray(site?.posts), ...toArray(site?.pages)];
87
+ }
88
+
89
+ function getHreflangLinks(site, page, config, helper) {
90
+ if (!isI18nEnabled(config)) return [];
91
+
92
+ const links = new Map();
93
+ const pageKey = getI18nKey(page);
94
+ const langKey = getPageLanguageKey(page, config);
95
+ const locale = getPageLocale(page, config);
96
+
97
+ addAlternateLink(links, locale, page.permalink || page.path, helper, config);
98
+ addExplicitAlternates(links, page.i18n?.alternates || page.alternates || page.hreflang, helper, config);
99
+
100
+ if (pageKey) {
101
+ collectDocuments(site).forEach((item) => {
102
+ if (!item || getI18nKey(item) !== pageKey) return;
103
+ addAlternateLink(links, getPageLocale(item, config), item.permalink || item.path, helper, config);
104
+ });
105
+ }
106
+
107
+ if (links.size > 1 && !links.has("x-default")) {
108
+ const defaultLanguage = getLanguage(config, getDefaultLanguageKey(config));
109
+ const defaultLink = Array.from(links.values()).find((link) => link.hreflang.toLowerCase() === defaultLanguage.locale.toLowerCase());
110
+ if (defaultLink) addAlternateLink(links, "x-default", defaultLink.href, helper, config);
111
+ }
112
+
113
+ return Array.from(links.values()).sort((a, b) => {
114
+ if (a.hreflang === "x-default") return 1;
115
+ if (b.hreflang === "x-default") return -1;
116
+ if (a.hreflang === getLanguage(config, langKey).locale) return -1;
117
+ if (b.hreflang === getLanguage(config, langKey).locale) return 1;
118
+ return a.hreflang.localeCompare(b.hreflang);
119
+ });
120
+ }
121
+
26
122
  module.exports = class extends Component {
27
123
  render() {
28
124
  const { site, config, helper, page } = this.props;
29
125
  const { url_for, is_post } = helper;
30
126
  const { url, head = {}, article } = config;
31
- const { meta = [], open_graph = {}, structured_data = {}, canonical_url = page.permalink, favicon } = head;
127
+ const { meta = [], open_graph = {}, structured_data = {}, canonical_url: headCanonicalUrl = page.permalink, favicon } = head;
128
+ const markdownSourceUrl = page.markdown_path ? url_for(page.markdown_path) : null;
129
+ const markdownSourceType = "text/markdown; charset=utf-8";
32
130
 
33
131
  const noIndex = helper.is_archive() || helper.is_tag();
34
132
 
35
- const language = page.lang || page.language || config.language;
133
+ const language = getPageLocale(page, config) || page.lang || page.language || config.language;
134
+ const canonicalUrl = toAbsoluteUrl(page.canonical_url || page.canonical || page.i18n?.canonical || headCanonicalUrl, helper, config);
135
+ const hreflangLinks = getHreflangLinks(site, page, config, helper);
36
136
 
37
137
  let images;
38
138
  if (typeof page.og_image === "string") {
@@ -68,31 +168,15 @@ module.exports = class extends Component {
68
168
  structuredImages = page.photos;
69
169
  }
70
170
 
71
- const themeInitScript = `
72
- (function() {
73
- var THEME_MAP = {
74
- mocha: "night",
75
- macchiato: "night",
76
- nord: "light",
77
- nord_night: "night",
78
- rose_pine: "night",
79
- tokyo_night: "night",
80
- latte: "light"
81
- };
82
- var stored = localStorage.getItem("themePreference");
83
- var theme = stored && stored in THEME_MAP ? stored : "system";
84
- var html = document.documentElement;
85
- var resolvedTheme = theme === "system"
86
- ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "mocha" : "nord"
87
- : theme;
88
- html.setAttribute("data-theme", resolvedTheme);
89
- html.classList.add(THEME_MAP[resolvedTheme]);
90
- })();
91
- `;
171
+ const themeInitScript = getThemeInitScript();
172
+ const articleFontInitScript = getArticleFontInitScript();
173
+ const articleFontUtilsScript = fs.readFileSync(path.join(__dirname, "../../source/js/article-font-utils.js"), "utf8");
92
174
 
93
175
  return (
94
176
  <head>
95
177
  <script dangerouslySetInnerHTML={{ __html: themeInitScript }}></script>
178
+ <script dangerouslySetInnerHTML={{ __html: articleFontUtilsScript }}></script>
179
+ <script dangerouslySetInnerHTML={{ __html: articleFontInitScript }}></script>
96
180
  <meta charset="utf-8" />
97
181
  <meta name="viewport" content="width=device-width, initial-scale=1" />
98
182
  {noIndex ? <meta name="robots" content="noindex" /> : null}
@@ -109,7 +193,11 @@ module.exports = class extends Component {
109
193
  keywords={(page.tags?.length ? page.tags : undefined) || config.keywords}
110
194
  url={open_graph.url || page.permalink || url}
111
195
  images={openGraphImages}
196
+ imageAlt={open_graph.image_alt || page.og_image_alt || page.cover_alt || page.title || config.title}
197
+ imageWidth={open_graph.image_width}
198
+ imageHeight={open_graph.image_height}
112
199
  siteName={open_graph.site_name || config.title}
200
+ section={open_graph.section || getArticleSection(page)}
113
201
  language={language}
114
202
  twitterId={open_graph.twitter_id}
115
203
  twitterCard={open_graph.twitter_card}
@@ -132,18 +220,19 @@ module.exports = class extends Component {
132
220
  images={structuredImages}
133
221
  />
134
222
  ) : null}
135
- {canonical_url ? <link rel="canonical" href={canonical_url} /> : null}
136
- {favicon ? <link rel="icon" href={url_for(favicon)} /> : null}
223
+ {canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
224
+ {hreflangLinks.map((link) => (
225
+ <link rel="alternate" hreflang={link.hreflang} href={link.href} />
226
+ ))}
227
+ {is_post(page) && markdownSourceUrl ? <link rel="alternate" type={markdownSourceType} title={helper.__("article.markdown_source")} href={markdownSourceUrl} /> : null}
228
+ <link rel="icon" href={url_for(favicon || "/img/favicon.svg")} />
137
229
  <link rel="stylesheet" href={url_for("/css/default.css")} />
138
- <link rel="stylesheet" href={url_for("/css/responsive/mobile.css")} media="screen and (max-width:768px)" />
139
- <link rel="stylesheet" href={url_for("/css/responsive/tablet.css")} media="screen and (min-width:769px)" />
140
- <link rel="stylesheet" href={url_for("/css/responsive/touch.css")} media="screen and (max-width:1023px)" />
141
- <link rel="stylesheet" href={url_for("/css/responsive/desktop.css")} media="screen and (min-width:1024px)" />
142
- <link rel="preload" as="style" href={url_for("/css/callout_blocks.css")} onload="this.onload=null;this.rel='stylesheet'" />
230
+ <link rel="stylesheet" href={url_for("/css/responsive.css")} />
231
+ <link rel="stylesheet" href={url_for("/css/callout_blocks.css")} media="print" onload="this.media='all'" />
143
232
  <link rel="preload" href={url_for("/css/font/woff2/HomemadeApple.woff2")} as="font" type="font/woff2" crossorigin />
144
233
  <link rel="preconnect" href="https://fontsapi.zeoseven.com" />
145
- <link rel="preload" as="style" href="https://fontsapi.zeoseven.com/285/main/result.css" onload="this.onload=null;this.rel='stylesheet'" />
146
- <link rel="preload" as="style" href="https://fontsapi.zeoseven.com/442/main/result.css" onload="this.onload=null;this.rel='stylesheet'" />
234
+ <link rel="stylesheet" href="https://fontsapi.zeoseven.com/5/main/result.css" media="print" onload="this.media='all'" />
235
+ <link rel="stylesheet" href="https://fontsapi.zeoseven.com/442/main/result.css" media="print" onload="this.media='all'" />
147
236
  <link rel="preload" as="style" href="/css/shiki/shiki.css" onload="this.onload=null;this.rel='stylesheet'" />
148
237
  {page.encrypt ? <link rel="stylesheet" href={url_for("/css/encrypt.css")} /> : null}
149
238
  <Plugins site={site} config={config} helper={helper} page={page} head={true} />