@shevky/core 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/package.json +59 -0
- package/scripts/analytics.js +86 -0
- package/scripts/build.js +1108 -0
- package/scripts/cli.js +95 -0
- package/scripts/init.js +126 -0
- package/scripts/main.js +48 -0
- package/scripts/social.js +205 -0
- package/shevky.js +3 -0
package/scripts/build.js
ADDED
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import Mustache from "mustache";
|
|
5
|
+
import { minify as minifyHtml } from "html-minifier-terser";
|
|
6
|
+
import {
|
|
7
|
+
io as _io,
|
|
8
|
+
i18n as _i18n,
|
|
9
|
+
config as _cfg,
|
|
10
|
+
log as _log,
|
|
11
|
+
format as _fmt,
|
|
12
|
+
plugin as _plugin,
|
|
13
|
+
} from "@shevky/base";
|
|
14
|
+
|
|
15
|
+
import _prj from "../lib/project.js";
|
|
16
|
+
import _analytics from "./analytics.js";
|
|
17
|
+
import _social from "./social.js";
|
|
18
|
+
|
|
19
|
+
import { MetaEngine } from "../engines/metaEngine.js";
|
|
20
|
+
import { RenderEngine } from "../engines/renderEngine.js";
|
|
21
|
+
|
|
22
|
+
import { PluginRegistry } from "../registries/pluginRegistry.js";
|
|
23
|
+
import {
|
|
24
|
+
TemplateRegistry,
|
|
25
|
+
TYPE_COMPONENT,
|
|
26
|
+
TYPE_LAYOUT,
|
|
27
|
+
TYPE_PARTIAL,
|
|
28
|
+
TYPE_TEMPLATE,
|
|
29
|
+
} from "../registries/templateRegistry.js";
|
|
30
|
+
import { ContentRegistry } from "../registries/contentRegistry.js";
|
|
31
|
+
import { PageRegistry } from "../registries/pageRegistry.js";
|
|
32
|
+
|
|
33
|
+
import { PluginEngine } from "../engines/pluginEngine.js";
|
|
34
|
+
import { MenuEngine } from "../engines/menuEngine.js";
|
|
35
|
+
|
|
36
|
+
/** @typedef {import("../lib/contentFile.js").ContentFile} ContentFile */
|
|
37
|
+
/** @typedef {import("../types/index.d.ts").FrontMatter} FrontMatter */
|
|
38
|
+
/** @typedef {import("../types/index.d.ts").CollectionEntry} CollectionEntry */
|
|
39
|
+
/** @typedef {import("../types/index.d.ts").CollectionsByLang} CollectionsByLang */
|
|
40
|
+
/** @typedef {import("../types/index.d.ts").FooterPolicy} FooterPolicy */
|
|
41
|
+
|
|
42
|
+
const SRC_DIR = _prj.srcDir;
|
|
43
|
+
const DIST_DIR = _prj.distDir;
|
|
44
|
+
const CONTENT_DIR = _prj.contentDir;
|
|
45
|
+
const LAYOUTS_DIR = _prj.layoutsDir;
|
|
46
|
+
const COMPONENTS_DIR = _prj.componentsDir;
|
|
47
|
+
const TEMPLATES_DIR = _prj.templatesDir;
|
|
48
|
+
const ASSETS_DIR = _prj.assetsDir;
|
|
49
|
+
const SITE_CONFIG_PATH = _prj.siteConfig;
|
|
50
|
+
const I18N_CONFIG_PATH = _prj.i18nConfig;
|
|
51
|
+
|
|
52
|
+
const pluginRegistry = new PluginRegistry();
|
|
53
|
+
const templateRegistry = new TemplateRegistry();
|
|
54
|
+
const pageRegistry = new PageRegistry();
|
|
55
|
+
|
|
56
|
+
const metaEngine = new MetaEngine();
|
|
57
|
+
const contentRegistry = new ContentRegistry(metaEngine);
|
|
58
|
+
const pluginEngine = new PluginEngine(
|
|
59
|
+
pluginRegistry,
|
|
60
|
+
contentRegistry,
|
|
61
|
+
metaEngine,
|
|
62
|
+
);
|
|
63
|
+
const menuEngine = new MenuEngine(contentRegistry, metaEngine);
|
|
64
|
+
const renderEngine = new RenderEngine({
|
|
65
|
+
templateRegistry,
|
|
66
|
+
metaEngine,
|
|
67
|
+
pageRegistry,
|
|
68
|
+
config: _cfg,
|
|
69
|
+
format: _fmt,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await _i18n.load(I18N_CONFIG_PATH);
|
|
73
|
+
await _cfg.load(SITE_CONFIG_PATH);
|
|
74
|
+
|
|
75
|
+
await templateRegistry.loadPartials(LAYOUTS_DIR);
|
|
76
|
+
await templateRegistry.loadComponents(COMPONENTS_DIR);
|
|
77
|
+
await templateRegistry.loadLayouts(LAYOUTS_DIR);
|
|
78
|
+
await templateRegistry.loadTemplates(TEMPLATES_DIR);
|
|
79
|
+
|
|
80
|
+
await pluginRegistry.load(_cfg.plugins);
|
|
81
|
+
|
|
82
|
+
const versionToken = crypto.randomBytes(6).toString("hex");
|
|
83
|
+
const DEFAULT_IMAGE = _cfg.seo.defaultImage;
|
|
84
|
+
/** @type {Record<string, string>} */
|
|
85
|
+
const FALLBACK_TAGLINES = { tr: "-", en: "-" };
|
|
86
|
+
/** @type {Record<string, any>} */
|
|
87
|
+
const COLLECTION_CONFIG = _cfg.content.collections;
|
|
88
|
+
|
|
89
|
+
const GENERATED_PAGES = new Set();
|
|
90
|
+
|
|
91
|
+
/** @type {CollectionsByLang} */
|
|
92
|
+
let PAGES = {};
|
|
93
|
+
/** @type {Record<string, FooterPolicy[]>} */
|
|
94
|
+
let FOOTER_POLICIES = {};
|
|
95
|
+
/** @type {Record<string, Record<string, { id: string, lang: string, title: string, canonical: string }>>} */
|
|
96
|
+
let CONTENT_INDEX = {};
|
|
97
|
+
renderEngine.setupMarkdown();
|
|
98
|
+
|
|
99
|
+
/** @param {unknown} input */
|
|
100
|
+
function byteLength(input) {
|
|
101
|
+
if (input === undefined || input === null) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof input !== "string") {
|
|
106
|
+
return Buffer.byteLength(String(input));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Buffer.byteLength(input);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {number} bytes */
|
|
113
|
+
function formatBytes(bytes) {
|
|
114
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
115
|
+
return "0B";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
119
|
+
let value = bytes;
|
|
120
|
+
let unitIndex = 0;
|
|
121
|
+
|
|
122
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
123
|
+
value /= 1024;
|
|
124
|
+
unitIndex += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const precision = unitIndex === 0 ? 0 : 2;
|
|
128
|
+
return `${value.toFixed(precision)}${units[unitIndex]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {string | null | undefined} pathValue */
|
|
132
|
+
function normalizeLogPath(pathValue) {
|
|
133
|
+
if (!pathValue) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const normalized = toPosixPath(pathValue);
|
|
138
|
+
if (normalized.startsWith("./")) {
|
|
139
|
+
return normalized.slice(2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @param {FrontMatter | { raw?: unknown } | null | undefined} front */
|
|
146
|
+
function normalizeFrontMatter(front) {
|
|
147
|
+
if (!front || typeof front !== "object") {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const raw =
|
|
152
|
+
"raw" in front && front.raw && typeof front.raw === "object"
|
|
153
|
+
? front.raw
|
|
154
|
+
: front;
|
|
155
|
+
|
|
156
|
+
return typeof raw === "object" && raw !== null ? { ...raw } : {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function ensureDist() {
|
|
160
|
+
await _io.directory.remove(DIST_DIR);
|
|
161
|
+
await _io.directory.create(DIST_DIR);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {string} html @param {string[]} locales */
|
|
165
|
+
function injectAlternateLocaleMeta(html, locales) {
|
|
166
|
+
const cleanupPattern =
|
|
167
|
+
/[^\S\r\n]*<meta property="og:locale:alternate" content=".*?" data-og-locale-alt\s*\/?>\s*/g;
|
|
168
|
+
const indentMatch = html.match(
|
|
169
|
+
/([^\S\r\n]*)<meta property="og:locale:alternate" content=".*?" data-og-locale-alt\s*\/?>/,
|
|
170
|
+
);
|
|
171
|
+
const indent = indentMatch?.[1] ?? " ";
|
|
172
|
+
let output = html.replace(cleanupPattern, "");
|
|
173
|
+
|
|
174
|
+
if (!locales.length) {
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const tags = locales
|
|
179
|
+
.map(
|
|
180
|
+
(locale) =>
|
|
181
|
+
`${indent}<meta property="og:locale:alternate" content="${locale}" data-og-locale-alt />`,
|
|
182
|
+
)
|
|
183
|
+
.join("\n");
|
|
184
|
+
const anchorPattern =
|
|
185
|
+
/(<meta property="og:locale" content=".*?" data-og-locale\s*\/?>)/;
|
|
186
|
+
|
|
187
|
+
if (anchorPattern.test(output)) {
|
|
188
|
+
return output.replace(anchorPattern, `$1\n${tags}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return `${tags}\n${output}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @param {string} lang */
|
|
195
|
+
function resolvePaginationSegment(lang) {
|
|
196
|
+
/** @type {Record<string, string>} */
|
|
197
|
+
const segmentConfig = _cfg?.content?.pagination?.segment ?? {};
|
|
198
|
+
if (
|
|
199
|
+
typeof segmentConfig[lang] === "string" &&
|
|
200
|
+
segmentConfig[lang].trim().length > 0
|
|
201
|
+
) {
|
|
202
|
+
return segmentConfig[lang].trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const defaultSegment = segmentConfig[_i18n.default];
|
|
206
|
+
if (typeof defaultSegment === "string" && defaultSegment.trim().length > 0) {
|
|
207
|
+
return defaultSegment.trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return "page";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {unknown} view */
|
|
214
|
+
function buildEasterEggPayload(view) {
|
|
215
|
+
if (!_cfg.build.debug) {
|
|
216
|
+
return "{}";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!view || typeof view !== "object") {
|
|
220
|
+
return "{}";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return metaEngine.serializeForInlineScript(view);
|
|
225
|
+
} catch {
|
|
226
|
+
return "{}";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {string} relativePath @param {string} html @param {{action?: string, source?: string, type?: string, lang?: string, template?: string, items?: number, page?: string | number, inputBytes?: number}} [meta] */
|
|
231
|
+
async function writeHtmlFile(relativePath, html, meta = {}) {
|
|
232
|
+
const destPath = _io.path.combine(DIST_DIR, relativePath);
|
|
233
|
+
await _io.directory.create(_io.path.name(destPath));
|
|
234
|
+
const payload = typeof html === "string" ? html : String(html ?? "");
|
|
235
|
+
await _io.file.write(destPath, payload);
|
|
236
|
+
const outputBytes = byteLength(payload);
|
|
237
|
+
|
|
238
|
+
_log.step(meta.action ?? "WRITE_HTML", {
|
|
239
|
+
target: normalizeLogPath(destPath),
|
|
240
|
+
source: normalizeLogPath(meta.source),
|
|
241
|
+
type: meta.type ?? "html",
|
|
242
|
+
lang: meta.lang,
|
|
243
|
+
template: meta.template,
|
|
244
|
+
items: meta.items,
|
|
245
|
+
page: meta.page,
|
|
246
|
+
input:
|
|
247
|
+
typeof meta.inputBytes === "number"
|
|
248
|
+
? formatBytes(meta.inputBytes)
|
|
249
|
+
: undefined,
|
|
250
|
+
output: formatBytes(outputBytes),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function flushPages() {
|
|
255
|
+
const pages = pageRegistry
|
|
256
|
+
.list()
|
|
257
|
+
.sort((a, b) => (a.outputPath || "").localeCompare(b.outputPath || ""));
|
|
258
|
+
for (const page of pages) {
|
|
259
|
+
if (!page.outputPath) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
await writeHtmlFile(page.outputPath, page.html, page.writeMeta ?? {});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @param {string} html @param {string} langKey */
|
|
267
|
+
function applyLanguageMetadata(html, langKey) {
|
|
268
|
+
const config = _i18n.build[langKey];
|
|
269
|
+
if (!config) {
|
|
270
|
+
return html;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const altLocales = metaEngine.normalizeAlternateLocales(config.altLocale);
|
|
274
|
+
|
|
275
|
+
let output = html
|
|
276
|
+
.replace(/(<html\b[^>]*\slang=")(.*?)"/, `$1${config.langAttr}"`)
|
|
277
|
+
.replace(
|
|
278
|
+
/(<meta name="language" content=")(.*?)"/,
|
|
279
|
+
`$1${config.metaLanguage}"`,
|
|
280
|
+
)
|
|
281
|
+
.replace(
|
|
282
|
+
/(<link rel="canonical" href=")(.*?)" data-canonical/,
|
|
283
|
+
`$1${config.canonical}" data-canonical`,
|
|
284
|
+
)
|
|
285
|
+
.replace(
|
|
286
|
+
/(<meta property="og:url" content=")(.*?)" data-og-url/,
|
|
287
|
+
`$1${config.canonical}" data-og-url`,
|
|
288
|
+
)
|
|
289
|
+
.replace(
|
|
290
|
+
/(<meta name="twitter:url" content=")(.*?)" data-twitter-url/,
|
|
291
|
+
`$1${config.canonical}" data-twitter-url`,
|
|
292
|
+
)
|
|
293
|
+
.replace(
|
|
294
|
+
/(<meta property="og:locale" content=")(.*?)" data-og-locale/,
|
|
295
|
+
`$1${config.ogLocale}" data-og-locale`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
output = injectAlternateLocaleMeta(output, altLocales);
|
|
299
|
+
return output;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @param {string} key @param {string} lang */
|
|
303
|
+
function buildTagSlug(key, lang) {
|
|
304
|
+
if (!key) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** @type {any} */
|
|
309
|
+
const tagsConfig = _cfg.content.collections.tags;
|
|
310
|
+
const slugPattern =
|
|
311
|
+
tagsConfig && typeof tagsConfig.slugPattern === "object"
|
|
312
|
+
? /** @type {Record<string, string>} */ (tagsConfig.slugPattern)
|
|
313
|
+
: {};
|
|
314
|
+
|
|
315
|
+
const langPattern =
|
|
316
|
+
typeof slugPattern[lang] === "string" ? slugPattern[lang] : null;
|
|
317
|
+
if (langPattern) {
|
|
318
|
+
return langPattern.includes("{{key}}")
|
|
319
|
+
? langPattern.replace("{{key}}", key)
|
|
320
|
+
: langPattern;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (lang === "en") {
|
|
324
|
+
return `tag/${key}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (lang === "tr") {
|
|
328
|
+
return `etiket/${key}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return key;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** @param {string} key @param {string} lang */
|
|
335
|
+
function buildTagUrlFromKey(key, lang) {
|
|
336
|
+
const slug = buildTagSlug(key, lang);
|
|
337
|
+
if (!slug) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return metaEngine.buildContentUrl(null, lang, slug);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @param {string} label @param {string} lang */
|
|
345
|
+
function buildTagUrlFromLabel(label, lang) {
|
|
346
|
+
const key = _fmt.slugify(label);
|
|
347
|
+
if (!key) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return buildTagUrlFromKey(key, lang);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @param {string} lang */
|
|
355
|
+
function buildFooterTags(lang) {
|
|
356
|
+
const langCollections = PAGES[lang] ?? {};
|
|
357
|
+
const limit = _cfg.seo.footerTagCount;
|
|
358
|
+
/** @type {Array<{ key: string, count: number, url: string }>} */
|
|
359
|
+
const results = [];
|
|
360
|
+
Object.keys(langCollections).forEach((key) => {
|
|
361
|
+
const items = langCollections[key] ?? [];
|
|
362
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const count = items.filter((entry) => entry.type === "tag").length;
|
|
367
|
+
if (count === 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const url = buildTagUrlFromKey(key, lang);
|
|
372
|
+
if (!url) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
results.push({ key, count, url });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
results.sort((a, b) => {
|
|
380
|
+
if (b.count !== a.count) {
|
|
381
|
+
return b.count - a.count;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return a.key.localeCompare(b.key, lang);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (limit && Number.isFinite(limit) && limit > 0) {
|
|
388
|
+
return results.slice(0, limit);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return results;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {string} lang */
|
|
395
|
+
function getFooterData(lang) {
|
|
396
|
+
const policiesSource =
|
|
397
|
+
FOOTER_POLICIES[lang] ?? FOOTER_POLICIES[_i18n.default] ?? [];
|
|
398
|
+
const tagsSource = buildFooterTags(lang);
|
|
399
|
+
const socialSource =
|
|
400
|
+
/** @type {Array<{ key: string, url: string, icon?: string }>} */ (
|
|
401
|
+
Array.isArray(_social.get()) ? _social.get() : []
|
|
402
|
+
);
|
|
403
|
+
const social = socialSource.filter(Boolean).map((item) => {
|
|
404
|
+
let url = item.url;
|
|
405
|
+
if (item.key === "rss") {
|
|
406
|
+
url = lang === _i18n.default ? "/feed.xml" : `/${lang}/feed.xml`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
...item,
|
|
411
|
+
url,
|
|
412
|
+
icon: item.icon,
|
|
413
|
+
label: _i18n.t(lang, `footer.social.${item.key}`, item.key.toUpperCase()),
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const tags = tagsSource.map((tag) => ({
|
|
418
|
+
...tag,
|
|
419
|
+
label: _i18n.t(lang, `footer.tags.${tag.key}`, tag.key),
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
const policies = policiesSource.map((policy) => ({
|
|
423
|
+
...policy,
|
|
424
|
+
label: _i18n.t(
|
|
425
|
+
lang,
|
|
426
|
+
`footer.policies.${policy.key}`,
|
|
427
|
+
policy.label ?? policy.key,
|
|
428
|
+
),
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
const tagline = _i18n.t(
|
|
432
|
+
lang,
|
|
433
|
+
"footer.tagline",
|
|
434
|
+
FALLBACK_TAGLINES[lang] ??
|
|
435
|
+
FALLBACK_TAGLINES[_i18n.default] ??
|
|
436
|
+
FALLBACK_TAGLINES.en,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
tags,
|
|
441
|
+
policies,
|
|
442
|
+
social,
|
|
443
|
+
tagline,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {string} templateName
|
|
449
|
+
* @param {string} contentHtml
|
|
450
|
+
* @param {FrontMatter} front
|
|
451
|
+
* @param {string} lang
|
|
452
|
+
* @param {Record<string, any>} dictionary
|
|
453
|
+
* @param {any} [listingOverride]
|
|
454
|
+
*/
|
|
455
|
+
async function renderContentTemplate(
|
|
456
|
+
templateName,
|
|
457
|
+
contentHtml,
|
|
458
|
+
front,
|
|
459
|
+
lang,
|
|
460
|
+
dictionary,
|
|
461
|
+
listingOverride,
|
|
462
|
+
) {
|
|
463
|
+
const template = templateRegistry.getTemplate(TYPE_TEMPLATE, templateName);
|
|
464
|
+
const baseFront = normalizeFrontMatter(front);
|
|
465
|
+
/** @type {string[]} */
|
|
466
|
+
const normalizedTags = Array.isArray(front.tags)
|
|
467
|
+
? front.tags.filter(
|
|
468
|
+
(/** @type {string} */ tag) =>
|
|
469
|
+
typeof tag === "string" && tag.trim().length > 0,
|
|
470
|
+
)
|
|
471
|
+
: [];
|
|
472
|
+
const tagLinks = normalizedTags
|
|
473
|
+
.map((/** @type {string} */ tag) => {
|
|
474
|
+
const url = buildTagUrlFromLabel(tag, lang);
|
|
475
|
+
return url ? { label: tag, url } : null;
|
|
476
|
+
})
|
|
477
|
+
.filter(Boolean);
|
|
478
|
+
const categorySlug =
|
|
479
|
+
typeof front.category === "string" && front.category.trim().length > 0
|
|
480
|
+
? _fmt.slugify(front.category)
|
|
481
|
+
: "";
|
|
482
|
+
const categoryUrl = categorySlug
|
|
483
|
+
? metaEngine.buildContentUrl(null, lang, categorySlug)
|
|
484
|
+
: null;
|
|
485
|
+
const resolvedDictionary = dictionary ?? _i18n.get(lang);
|
|
486
|
+
const normalizedFront = /** @type {FrontMatter} */ ({
|
|
487
|
+
...baseFront,
|
|
488
|
+
tags: normalizedTags,
|
|
489
|
+
tagLinks,
|
|
490
|
+
hasTags: normalizedTags.length > 0,
|
|
491
|
+
categoryUrl,
|
|
492
|
+
categoryLabel:
|
|
493
|
+
typeof front.category === "string" && front.category.trim().length > 0
|
|
494
|
+
? front.category.trim()
|
|
495
|
+
: "",
|
|
496
|
+
dateDisplay: _fmt.date(front.date, lang),
|
|
497
|
+
updatedDisplay: _fmt.date(front.updated, lang),
|
|
498
|
+
cover: front.cover ?? DEFAULT_IMAGE,
|
|
499
|
+
coverAlt: front.coverAlt ?? "",
|
|
500
|
+
lang,
|
|
501
|
+
});
|
|
502
|
+
if (front?.collectionType) {
|
|
503
|
+
normalizedFront.collectionType = normalizeCollectionTypeValue(
|
|
504
|
+
front.collectionType,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
normalizedFront.seriesListing = buildSeriesListing(normalizedFront, lang);
|
|
508
|
+
const listing =
|
|
509
|
+
listingOverride ?? buildCollectionListing(normalizedFront, lang);
|
|
510
|
+
const collectionFlags = buildCollectionTypeFlags(
|
|
511
|
+
listing?.type ?? resolveCollectionType(normalizedFront, listing?.items),
|
|
512
|
+
);
|
|
513
|
+
const site = metaEngine.buildSiteData(lang);
|
|
514
|
+
const languageFlags = _i18n.flags(lang);
|
|
515
|
+
|
|
516
|
+
return Mustache.render(
|
|
517
|
+
template.content,
|
|
518
|
+
{
|
|
519
|
+
content: { html: decorateHtml(contentHtml, templateName) },
|
|
520
|
+
front: normalizedFront,
|
|
521
|
+
lang,
|
|
522
|
+
listing,
|
|
523
|
+
site,
|
|
524
|
+
locale: languageFlags.locale,
|
|
525
|
+
isEnglish: languageFlags.isEnglish,
|
|
526
|
+
isTurkish: languageFlags.isTurkish,
|
|
527
|
+
i18n: resolvedDictionary,
|
|
528
|
+
...collectionFlags,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
...templateRegistry.getFiles(TYPE_PARTIAL),
|
|
532
|
+
...templateRegistry.getFiles(TYPE_COMPONENT),
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @param {FrontMatter} frontMatter
|
|
539
|
+
* @param {string} lang
|
|
540
|
+
* @param {Record<string, any>} dictionary
|
|
541
|
+
*/
|
|
542
|
+
/**
|
|
543
|
+
* @param {{ layoutName: string, view: Record<string, any>, front: FrontMatter, lang: string, slug: string, writeMeta?: { action?: string, source?: string, type?: string, lang?: string, template?: string, items?: number, page?: string | number, inputBytes?: number } }} input
|
|
544
|
+
*/
|
|
545
|
+
async function renderPage({ layoutName, view, front, lang, slug, writeMeta }) {
|
|
546
|
+
const rendered = renderEngine.renderLayout(layoutName, view);
|
|
547
|
+
const finalHtml = await renderEngine.transformHtml(rendered, {
|
|
548
|
+
versionToken,
|
|
549
|
+
minifyHtml,
|
|
550
|
+
});
|
|
551
|
+
const relativePath = buildOutputPath(front, lang, slug);
|
|
552
|
+
const page = renderEngine.createPage({
|
|
553
|
+
kind: "page",
|
|
554
|
+
type:
|
|
555
|
+
typeof writeMeta?.type === "string" && writeMeta.type.length > 0
|
|
556
|
+
? writeMeta.type
|
|
557
|
+
: typeof front?.template === "string"
|
|
558
|
+
? front.template
|
|
559
|
+
: "",
|
|
560
|
+
lang,
|
|
561
|
+
slug,
|
|
562
|
+
canonical: metaEngine.buildContentUrl(front?.canonical, lang, slug),
|
|
563
|
+
layout: layoutName,
|
|
564
|
+
template: typeof front?.template === "string" ? front.template : "",
|
|
565
|
+
front,
|
|
566
|
+
view,
|
|
567
|
+
html: finalHtml,
|
|
568
|
+
sourcePath: typeof writeMeta?.source === "string" ? writeMeta.source : "",
|
|
569
|
+
outputPath: relativePath,
|
|
570
|
+
writeMeta,
|
|
571
|
+
});
|
|
572
|
+
GENERATED_PAGES.add(toPosixPath(relativePath));
|
|
573
|
+
registerLegacyPaths(lang, slug);
|
|
574
|
+
return page;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** @param {string} html @param {string} templateName */
|
|
578
|
+
function decorateHtml(html, templateName) {
|
|
579
|
+
return html;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** @param {FrontMatter} front @param {string} lang @param {string} slug */
|
|
583
|
+
function buildOutputPath(front, lang, slug) {
|
|
584
|
+
const canonicalRelative = metaEngine.canonicalToRelativePath(front.canonical);
|
|
585
|
+
if (canonicalRelative) {
|
|
586
|
+
return _io.path.combine(canonicalRelative, "index.html");
|
|
587
|
+
}
|
|
588
|
+
const cleaned = (slug ?? "").replace(/^\/+/, "");
|
|
589
|
+
/** @type {string[]} */
|
|
590
|
+
const segments = [];
|
|
591
|
+
if (lang && lang !== _i18n.default) {
|
|
592
|
+
segments.push(lang);
|
|
593
|
+
}
|
|
594
|
+
if (cleaned) {
|
|
595
|
+
segments.push(cleaned);
|
|
596
|
+
}
|
|
597
|
+
return _io.path.combine(...segments.filter(Boolean), "index.html");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** @param {string} value */
|
|
601
|
+
function toPosixPath(value) {
|
|
602
|
+
return value.split(_io.path.separator).join("/");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** @param {FrontMatter} front @param {string} lang */
|
|
606
|
+
function buildCollectionListing(front, lang) {
|
|
607
|
+
const normalizedLang = lang ?? _i18n.default;
|
|
608
|
+
const langCollections = PAGES[normalizedLang] ?? {};
|
|
609
|
+
const key = resolveListingKey(front);
|
|
610
|
+
const sourceItems =
|
|
611
|
+
key && Array.isArray(langCollections[key]) ? langCollections[key] : [];
|
|
612
|
+
const items = dedupeCollectionItems(sourceItems);
|
|
613
|
+
const collectionType = resolveCollectionType(front, items);
|
|
614
|
+
const typeFlags = buildCollectionTypeFlags(collectionType);
|
|
615
|
+
return {
|
|
616
|
+
key,
|
|
617
|
+
lang: normalizedLang,
|
|
618
|
+
items,
|
|
619
|
+
hasItems: items.length > 0,
|
|
620
|
+
emptyMessage: resolveListingEmpty(front, normalizedLang),
|
|
621
|
+
heading: resolveListingHeading(front),
|
|
622
|
+
type: collectionType,
|
|
623
|
+
...typeFlags,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** @param {FrontMatter} front @param {string} lang */
|
|
628
|
+
function buildSeriesListing(front, lang) {
|
|
629
|
+
/** @type {string[]} */
|
|
630
|
+
const relatedSource = Array.isArray(front?.related) ? front.related : [];
|
|
631
|
+
const seriesName =
|
|
632
|
+
typeof front?.seriesTitle === "string" &&
|
|
633
|
+
front.seriesTitle.trim().length > 0
|
|
634
|
+
? front.seriesTitle.trim()
|
|
635
|
+
: typeof front?.series === "string"
|
|
636
|
+
? front.series.trim()
|
|
637
|
+
: "";
|
|
638
|
+
const currentId = typeof front?.id === "string" ? front.id.trim() : "";
|
|
639
|
+
/** @type {Array<{ id: string, label: string, url: string, hasUrl?: boolean, isCurrent: boolean, isPlaceholder: boolean }>} */
|
|
640
|
+
const items = [];
|
|
641
|
+
|
|
642
|
+
relatedSource.forEach((/** @type {string} */ entry) => {
|
|
643
|
+
const value = typeof entry === "string" ? entry.trim() : "";
|
|
644
|
+
if (!value) {
|
|
645
|
+
items.push({
|
|
646
|
+
id: "",
|
|
647
|
+
label: "...",
|
|
648
|
+
url: "",
|
|
649
|
+
isCurrent: false,
|
|
650
|
+
isPlaceholder: true,
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const isCurrent = value === currentId;
|
|
656
|
+
const summaryLookup = CONTENT_INDEX[value];
|
|
657
|
+
const summaryLang = lang || front?.lang;
|
|
658
|
+
let summary = null;
|
|
659
|
+
if (summaryLookup) {
|
|
660
|
+
const summaryFallback =
|
|
661
|
+
/** @type {{ title?: string, canonical?: string }} */ (
|
|
662
|
+
Object.values(summaryLookup)[0]
|
|
663
|
+
);
|
|
664
|
+
summary =
|
|
665
|
+
summaryLookup[summaryLang] ??
|
|
666
|
+
summaryLookup[front?.lang] ??
|
|
667
|
+
summaryFallback;
|
|
668
|
+
}
|
|
669
|
+
const label =
|
|
670
|
+
summary?.title ?? (isCurrent ? (front?.title ?? value) : value);
|
|
671
|
+
const url = summary?.canonical ?? "";
|
|
672
|
+
|
|
673
|
+
const hasUrl = typeof url === "string" && url.length > 0;
|
|
674
|
+
items.push({
|
|
675
|
+
id: value,
|
|
676
|
+
label,
|
|
677
|
+
url,
|
|
678
|
+
hasUrl,
|
|
679
|
+
isCurrent,
|
|
680
|
+
isPlaceholder: false,
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
label: seriesName,
|
|
686
|
+
hasLabel: Boolean(seriesName),
|
|
687
|
+
hasItems: items.length > 0,
|
|
688
|
+
items,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** @param {unknown} value */
|
|
693
|
+
function normalizeCollectionTypeValue(value) {
|
|
694
|
+
if (typeof value !== "string") {
|
|
695
|
+
return "";
|
|
696
|
+
}
|
|
697
|
+
return value.trim().toLowerCase();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** @param {FrontMatter} front @param {CollectionEntry[] | undefined} items @param {string} [fallback] */
|
|
701
|
+
function resolveCollectionType(front, items, fallback) {
|
|
702
|
+
const explicitCandidate =
|
|
703
|
+
normalizeCollectionTypeValue(front?.collectionType) ||
|
|
704
|
+
normalizeCollectionTypeValue(front?.listType) ||
|
|
705
|
+
normalizeCollectionTypeValue(front?.type);
|
|
706
|
+
if (explicitCandidate) {
|
|
707
|
+
return explicitCandidate;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (Array.isArray(items)) {
|
|
711
|
+
const entryWithType = items.find(
|
|
712
|
+
(entry) =>
|
|
713
|
+
typeof entry?.type === "string" && entry.type.trim().length > 0,
|
|
714
|
+
);
|
|
715
|
+
const entryType =
|
|
716
|
+
typeof entryWithType?.type === "string" ? entryWithType.type.trim() : "";
|
|
717
|
+
if (entryType) {
|
|
718
|
+
return entryType.toLowerCase();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (typeof fallback === "string" && fallback.trim().length > 0) {
|
|
723
|
+
return fallback.trim().toLowerCase();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return "";
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** @param {string} type */
|
|
730
|
+
function buildCollectionTypeFlags(type) {
|
|
731
|
+
const normalized = normalizeCollectionTypeValue(type);
|
|
732
|
+
return {
|
|
733
|
+
collectionType: normalized,
|
|
734
|
+
isTag: normalized === "tag",
|
|
735
|
+
isCategory: normalized === "category",
|
|
736
|
+
isAuthor: normalized === "author",
|
|
737
|
+
isSeries: normalized === "series",
|
|
738
|
+
isHome: normalized === "home",
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** @param {FrontMatter} front */
|
|
743
|
+
function resolveListingKey(front) {
|
|
744
|
+
if (!front) return "";
|
|
745
|
+
const candidates = [
|
|
746
|
+
typeof front.listKey === "string" ? front.listKey : null,
|
|
747
|
+
typeof front.slug === "string" ? front.slug : null,
|
|
748
|
+
typeof front.category === "string" ? front.category : null,
|
|
749
|
+
typeof front.id === "string" ? front.id : null,
|
|
750
|
+
];
|
|
751
|
+
for (const value of candidates) {
|
|
752
|
+
if (typeof value !== "string") continue;
|
|
753
|
+
const normalized = _fmt.slugify(value);
|
|
754
|
+
if (normalized) {
|
|
755
|
+
return normalized;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return "";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** @param {FrontMatter} front @param {string} lang */
|
|
762
|
+
function resolveListingEmpty(front, lang) {
|
|
763
|
+
if (!front) return "";
|
|
764
|
+
const { listingEmpty } = front;
|
|
765
|
+
if (typeof listingEmpty === "string" && listingEmpty.trim().length > 0) {
|
|
766
|
+
return listingEmpty.trim();
|
|
767
|
+
}
|
|
768
|
+
if (listingEmpty && typeof listingEmpty === "object") {
|
|
769
|
+
const listingEmptyMap = /** @type {Record<string, string>} */ (
|
|
770
|
+
listingEmpty
|
|
771
|
+
);
|
|
772
|
+
const localized = listingEmptyMap[lang];
|
|
773
|
+
if (typeof localized === "string" && localized.trim().length > 0) {
|
|
774
|
+
return localized.trim();
|
|
775
|
+
}
|
|
776
|
+
const fallback = listingEmptyMap[_i18n.default];
|
|
777
|
+
if (typeof fallback === "string" && fallback.trim().length > 0) {
|
|
778
|
+
return fallback.trim();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return "";
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** @param {FrontMatter} front */
|
|
785
|
+
function resolveListingHeading(front) {
|
|
786
|
+
if (!front) return "";
|
|
787
|
+
if (
|
|
788
|
+
typeof front.listHeading === "string" &&
|
|
789
|
+
front.listHeading.trim().length > 0
|
|
790
|
+
) {
|
|
791
|
+
return front.listHeading.trim();
|
|
792
|
+
}
|
|
793
|
+
if (typeof front.title === "string" && front.title.trim().length > 0) {
|
|
794
|
+
return front.title.trim();
|
|
795
|
+
}
|
|
796
|
+
return "";
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function buildContentPages() {
|
|
800
|
+
if (contentRegistry.count === 0) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const contentFiles = /** @type {ContentFile[]} */ (
|
|
805
|
+
/** @type {unknown} */ (contentRegistry.files)
|
|
806
|
+
);
|
|
807
|
+
for (const file of contentFiles) {
|
|
808
|
+
_log.step("PROCESS_CONTENT", {
|
|
809
|
+
file: normalizeLogPath(file.sourcePath),
|
|
810
|
+
lang: file.lang,
|
|
811
|
+
template: file.template,
|
|
812
|
+
size: formatBytes(byteLength(file.content)),
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
const dictionary = _i18n.get(file.lang);
|
|
816
|
+
const componentContext = renderEngine.buildContentComponentContext(
|
|
817
|
+
file.header,
|
|
818
|
+
file.lang,
|
|
819
|
+
dictionary,
|
|
820
|
+
{ i18n: _i18n, pages: PAGES },
|
|
821
|
+
);
|
|
822
|
+
const { markdown: markdownSource, placeholders } =
|
|
823
|
+
renderEngine.renderMarkdownComponents(file.content, componentContext);
|
|
824
|
+
if (placeholders.length > 0) {
|
|
825
|
+
_log.step("COMPONENT_SLOTS", {
|
|
826
|
+
file: normalizeLogPath(file.sourcePath),
|
|
827
|
+
count: placeholders.length,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const markdownHtml = renderEngine.parseMarkdown(markdownSource ?? "");
|
|
832
|
+
const hydratedHtml = renderEngine.injectMarkdownComponents(
|
|
833
|
+
markdownHtml ?? "",
|
|
834
|
+
placeholders,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
if (file.template === "collection" || file.template === "home") {
|
|
838
|
+
await renderEngine.buildPaginatedCollectionPages({
|
|
839
|
+
frontMatter: file.header,
|
|
840
|
+
lang: file.lang,
|
|
841
|
+
baseSlug: file.slug,
|
|
842
|
+
layoutName: file.layout,
|
|
843
|
+
templateName: file.template,
|
|
844
|
+
contentHtml: hydratedHtml,
|
|
845
|
+
dictionary,
|
|
846
|
+
sourcePath: file.sourcePath,
|
|
847
|
+
pages: PAGES,
|
|
848
|
+
renderContentTemplate,
|
|
849
|
+
buildViewPayload: (input) =>
|
|
850
|
+
renderEngine.buildViewPayload(input, {
|
|
851
|
+
pages: PAGES,
|
|
852
|
+
i18n: _i18n,
|
|
853
|
+
metaEngine,
|
|
854
|
+
menuEngine,
|
|
855
|
+
getFooterData,
|
|
856
|
+
analyticsSnippets: _analytics.snippets,
|
|
857
|
+
buildEasterEggPayload,
|
|
858
|
+
}),
|
|
859
|
+
renderPage,
|
|
860
|
+
metaEngine,
|
|
861
|
+
menuEngine,
|
|
862
|
+
resolveListingKey,
|
|
863
|
+
resolveListingEmpty,
|
|
864
|
+
resolveCollectionType,
|
|
865
|
+
buildCollectionTypeFlags,
|
|
866
|
+
resolvePaginationSegment,
|
|
867
|
+
dedupeCollectionItems,
|
|
868
|
+
byteLength,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const contentHtml = await renderContentTemplate(
|
|
875
|
+
file.template,
|
|
876
|
+
hydratedHtml,
|
|
877
|
+
file.header,
|
|
878
|
+
file.lang,
|
|
879
|
+
dictionary,
|
|
880
|
+
);
|
|
881
|
+
const pageMeta = metaEngine.buildPageMeta(
|
|
882
|
+
file.header,
|
|
883
|
+
file.lang,
|
|
884
|
+
file.slug,
|
|
885
|
+
);
|
|
886
|
+
const activeMenuKey = menuEngine.resolveActiveMenuKey(file.header);
|
|
887
|
+
const view = renderEngine.buildViewPayload(
|
|
888
|
+
{
|
|
889
|
+
lang: file.lang,
|
|
890
|
+
activeMenuKey,
|
|
891
|
+
pageMeta,
|
|
892
|
+
content: contentHtml,
|
|
893
|
+
dictionary,
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
pages: PAGES,
|
|
897
|
+
i18n: _i18n,
|
|
898
|
+
metaEngine,
|
|
899
|
+
menuEngine,
|
|
900
|
+
getFooterData,
|
|
901
|
+
analyticsSnippets: _analytics.snippets,
|
|
902
|
+
buildEasterEggPayload,
|
|
903
|
+
},
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
await renderPage({
|
|
907
|
+
layoutName: file.layout,
|
|
908
|
+
view,
|
|
909
|
+
front: file.header,
|
|
910
|
+
lang: file.lang,
|
|
911
|
+
slug: file.slug,
|
|
912
|
+
writeMeta: {
|
|
913
|
+
action: "BUILD_PAGE",
|
|
914
|
+
type: file.template,
|
|
915
|
+
source: file.sourcePath,
|
|
916
|
+
lang: file.lang,
|
|
917
|
+
template: file.layout,
|
|
918
|
+
inputBytes: byteLength(file.content),
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
await renderEngine.buildDynamicCollectionPages({
|
|
924
|
+
collectionsConfig: COLLECTION_CONFIG,
|
|
925
|
+
pages: PAGES,
|
|
926
|
+
i18n: _i18n,
|
|
927
|
+
renderContentTemplate,
|
|
928
|
+
buildViewPayload: (input) =>
|
|
929
|
+
renderEngine.buildViewPayload(input, {
|
|
930
|
+
pages: PAGES,
|
|
931
|
+
i18n: _i18n,
|
|
932
|
+
metaEngine,
|
|
933
|
+
menuEngine,
|
|
934
|
+
getFooterData,
|
|
935
|
+
analyticsSnippets: _analytics.snippets,
|
|
936
|
+
buildEasterEggPayload,
|
|
937
|
+
}),
|
|
938
|
+
renderPage,
|
|
939
|
+
metaEngine,
|
|
940
|
+
menuEngine,
|
|
941
|
+
resolveCollectionType,
|
|
942
|
+
normalizeCollectionTypeValue,
|
|
943
|
+
resolveCollectionDisplayKey,
|
|
944
|
+
dedupeCollectionItems,
|
|
945
|
+
normalizeLogPath,
|
|
946
|
+
io: _io,
|
|
947
|
+
byteLength,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** @param {string} configKey @param {string} defaultKey @param {CollectionEntry[]} items */
|
|
952
|
+
function resolveCollectionDisplayKey(configKey, defaultKey, items) {
|
|
953
|
+
if (configKey === "series" && Array.isArray(items)) {
|
|
954
|
+
const entryWithTitle = items.find(
|
|
955
|
+
(entry) =>
|
|
956
|
+
entry &&
|
|
957
|
+
typeof entry.seriesTitle === "string" &&
|
|
958
|
+
entry.seriesTitle.trim().length > 0,
|
|
959
|
+
);
|
|
960
|
+
const seriesTitle =
|
|
961
|
+
typeof entryWithTitle?.seriesTitle === "string"
|
|
962
|
+
? entryWithTitle.seriesTitle.trim()
|
|
963
|
+
: "";
|
|
964
|
+
if (seriesTitle) {
|
|
965
|
+
return seriesTitle;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return defaultKey;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** @param {CollectionEntry[]} items */
|
|
972
|
+
function dedupeCollectionItems(items) {
|
|
973
|
+
if (!Array.isArray(items) || items.length === 0) return items;
|
|
974
|
+
const seen = new Map();
|
|
975
|
+
/** @type {CollectionEntry[]} */
|
|
976
|
+
const order = [];
|
|
977
|
+
items.forEach((item) => {
|
|
978
|
+
const id = item?.id;
|
|
979
|
+
if (!id) {
|
|
980
|
+
order.push(item);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const existingIndex = seen.get(id);
|
|
984
|
+
const hasSeriesTitle = Boolean(item?.seriesTitle);
|
|
985
|
+
if (existingIndex == null) {
|
|
986
|
+
seen.set(id, order.length);
|
|
987
|
+
order.push(item);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const existing = order[existingIndex];
|
|
991
|
+
const existingHasSeries = Boolean(existing?.seriesTitle);
|
|
992
|
+
if (hasSeriesTitle && !existingHasSeries) {
|
|
993
|
+
order[existingIndex] = item;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
return order;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** @param {string} lang @param {string} slug */
|
|
1000
|
+
function registerLegacyPaths(lang, slug) {
|
|
1001
|
+
const cleaned = (slug ?? "").replace(/^\/+/, "");
|
|
1002
|
+
if (!cleaned) return;
|
|
1003
|
+
const legacyFile = cleaned.endsWith(".html") ? cleaned : `${cleaned}.html`;
|
|
1004
|
+
GENERATED_PAGES.add(toPosixPath(legacyFile));
|
|
1005
|
+
if (lang && lang !== _i18n.default) {
|
|
1006
|
+
GENERATED_PAGES.add(toPosixPath(_io.path.combine(lang, legacyFile)));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** @param {string} currentDir @param {string} relative */
|
|
1011
|
+
async function copyHtmlRecursive(currentDir = SRC_DIR, relative = "") {
|
|
1012
|
+
if (!(await _io.directory.exists(currentDir))) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const entries = await _io.directory.read(currentDir);
|
|
1017
|
+
for (const entry of entries) {
|
|
1018
|
+
const fullPath = _io.path.combine(currentDir, entry);
|
|
1019
|
+
const relPath = relative ? _io.path.combine(relative, entry) : entry;
|
|
1020
|
+
|
|
1021
|
+
if (!entry.endsWith(".html")) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (GENERATED_PAGES.has(toPosixPath(relPath))) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const raw = await _io.file.read(fullPath);
|
|
1030
|
+
const transformed = await renderEngine.transformHtml(raw, {
|
|
1031
|
+
versionToken,
|
|
1032
|
+
minifyHtml,
|
|
1033
|
+
});
|
|
1034
|
+
if (relPath === "index.html") {
|
|
1035
|
+
_i18n.supported.forEach(async (langCode) => {
|
|
1036
|
+
const localized = applyLanguageMetadata(transformed, langCode);
|
|
1037
|
+
/** @type {string[]} */
|
|
1038
|
+
const segments = [];
|
|
1039
|
+
|
|
1040
|
+
if (langCode !== _i18n.default) {
|
|
1041
|
+
segments.push(langCode);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
segments.push("index.html");
|
|
1045
|
+
await writeHtmlFile(_io.path.combine(...segments), localized, {
|
|
1046
|
+
action: "COPY_HTML",
|
|
1047
|
+
type: "static",
|
|
1048
|
+
source: fullPath,
|
|
1049
|
+
lang: langCode,
|
|
1050
|
+
inputBytes: byteLength(transformed),
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
await writeHtmlFile(relPath, transformed, {
|
|
1058
|
+
action: "COPY_HTML",
|
|
1059
|
+
type: "static",
|
|
1060
|
+
source: fullPath,
|
|
1061
|
+
lang: _i18n.default,
|
|
1062
|
+
inputBytes: byteLength(transformed),
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function copyStaticAssets() {
|
|
1068
|
+
if (!(await _io.directory.exists(ASSETS_DIR))) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const targetDir = _io.path.combine(DIST_DIR, "assets");
|
|
1073
|
+
await _io.directory.copy(ASSETS_DIR, targetDir);
|
|
1074
|
+
_log.debug("Assets have been copied.");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function main() {
|
|
1078
|
+
// <--- dist:clean
|
|
1079
|
+
await ensureDist();
|
|
1080
|
+
await pluginEngine.execute(_plugin.hooks.DIST_CLEAN);
|
|
1081
|
+
// dist:clean --->
|
|
1082
|
+
|
|
1083
|
+
// <--- assets:copy
|
|
1084
|
+
await copyStaticAssets();
|
|
1085
|
+
await pluginEngine.execute(_plugin.hooks.ASSETS_COPY);
|
|
1086
|
+
// assets:copy --->
|
|
1087
|
+
|
|
1088
|
+
// <--- content:load
|
|
1089
|
+
await contentRegistry.load(CONTENT_DIR);
|
|
1090
|
+
await pluginEngine.execute(_plugin.hooks.CONTENT_LOAD);
|
|
1091
|
+
// content:load --->
|
|
1092
|
+
|
|
1093
|
+
await menuEngine.build();
|
|
1094
|
+
PAGES = contentRegistry.buildCategoryTagCollections();
|
|
1095
|
+
FOOTER_POLICIES = contentRegistry.buildFooterPolicies();
|
|
1096
|
+
CONTENT_INDEX = contentRegistry.buildContentIndex();
|
|
1097
|
+
await pluginEngine.execute(_plugin.hooks.CONTENT_READY);
|
|
1098
|
+
|
|
1099
|
+
await buildContentPages();
|
|
1100
|
+
await copyHtmlRecursive();
|
|
1101
|
+
await flushPages();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const API = {
|
|
1105
|
+
execute: main,
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
export default API;
|