@shevky/core 0.0.6 → 0.0.8
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/README.md +50 -0
- package/engines/pluginEngine.js +24 -1
- package/engines/renderEngine.js +45 -13
- package/lib/contentFile.js +4 -0
- package/lib/contentHeader.js +14 -3
- package/package.json +9 -9
- package/registries/contentRegistry.js +59 -8
- package/scripts/build.js +403 -97
- package/scripts/main.js +1 -1
- package/types/index.d.ts +7 -0
package/README.md
CHANGED
|
@@ -23,6 +23,56 @@ npm run build
|
|
|
23
23
|
npm run dev
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
## Build Memory Setting
|
|
27
|
+
|
|
28
|
+
For large projects you can limit in-memory page buffering during build:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"build": {
|
|
33
|
+
"pageBufferLimit": 20
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `pageBufferLimit`: Number of rendered pages kept in memory before flushing to disk.
|
|
39
|
+
- Default: `20`
|
|
40
|
+
- Environment override: `SHEVKY_PAGE_BUFFER_LIMIT`
|
|
41
|
+
|
|
42
|
+
## Output Aliases
|
|
43
|
+
|
|
44
|
+
If you want to keep markdown generation (plugins/layout/meta) but publish an
|
|
45
|
+
additional file path, you can define build aliases:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"build": {
|
|
50
|
+
"outputAliases": [
|
|
51
|
+
{ "from": "~/404/", "to": "~/404.html" },
|
|
52
|
+
{ "from": "~/en/404/", "to": "~/en/404.html" }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `from`: Generated page URL/path.
|
|
59
|
+
- `to`: Extra copied output URL/path.
|
|
60
|
+
|
|
61
|
+
## Content Root Directories
|
|
62
|
+
|
|
63
|
+
You can copy selected `src/content` directories directly to dist root:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"build": {
|
|
68
|
+
"contentRootDirectories": [".well-known"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Default: `[".well-known"]`
|
|
74
|
+
- Example: `src/content/.well-known/apple-app-site-association` -> `dist/.well-known/apple-app-site-association`
|
|
75
|
+
|
|
26
76
|
## Technical Documentation
|
|
27
77
|
|
|
28
78
|
The tech documentation is preparing for Shevky with Shevky. You will find it under [Shevky Project](https://tatoglu.net/project/shevky) page.
|
package/engines/pluginEngine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log as _log, plugin as _plugin } from "@shevky/base";
|
|
1
|
+
import { i18n as _i18n, log as _log, plugin as _plugin } from "@shevky/base";
|
|
2
2
|
import { PluginRegistry } from "../registries/pluginRegistry.js";
|
|
3
3
|
import { ContentRegistry } from "../registries/contentRegistry.js";
|
|
4
4
|
import { Project } from "../lib/project.js";
|
|
@@ -27,6 +27,11 @@ export class PluginEngine {
|
|
|
27
27
|
*/
|
|
28
28
|
#_metaEngine;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @type {Record<string, any>}
|
|
32
|
+
*/
|
|
33
|
+
#_runtimeContext = {};
|
|
34
|
+
|
|
30
35
|
/**
|
|
31
36
|
* @param {PluginRegistry} pluginRegistry
|
|
32
37
|
* @param {ContentRegistry} contentRegistry
|
|
@@ -38,6 +43,22 @@ export class PluginEngine {
|
|
|
38
43
|
this.#_metaEngine = metaEngine;
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @param {Record<string, any>} runtimeContext
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
50
|
+
setRuntimeContext(runtimeContext) {
|
|
51
|
+
this.#_runtimeContext =
|
|
52
|
+
runtimeContext && typeof runtimeContext === "object" ? runtimeContext : {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
clearRuntimeContext() {
|
|
59
|
+
this.#_runtimeContext = {};
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
/**
|
|
42
63
|
* @param {string} hook
|
|
43
64
|
* @returns {Promise<void>}
|
|
@@ -82,6 +103,7 @@ export class PluginEngine {
|
|
|
82
103
|
return {
|
|
83
104
|
...baseContext,
|
|
84
105
|
paths: this.#_project.toObject(),
|
|
106
|
+
i18n: _i18n,
|
|
85
107
|
|
|
86
108
|
// content:load
|
|
87
109
|
...(hook === _plugin.hooks.CONTENT_LOAD
|
|
@@ -98,6 +120,7 @@ export class PluginEngine {
|
|
|
98
120
|
contentIndex: this.#_contentRegistry.buildContentIndex(),
|
|
99
121
|
}
|
|
100
122
|
: {}),
|
|
123
|
+
...this.#_runtimeContext,
|
|
101
124
|
};
|
|
102
125
|
}
|
|
103
126
|
}
|
package/engines/renderEngine.js
CHANGED
|
@@ -16,6 +16,33 @@ import { PageRegistry } from "../registries/pageRegistry.js";
|
|
|
16
16
|
|
|
17
17
|
/** @typedef {import("../types/index.d.ts").Placeholder} Placeholder */
|
|
18
18
|
|
|
19
|
+
const PAGINATED_SCHEMA_TYPE_RULES = {
|
|
20
|
+
home: "collection",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** @param {unknown} value */
|
|
24
|
+
function normalizeSchemaType(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value.trim().toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {unknown} schemaType @param {number} pageIndex */
|
|
33
|
+
function resolvePaginatedSchemaType(schemaType, pageIndex) {
|
|
34
|
+
const normalizedType = normalizeSchemaType(schemaType);
|
|
35
|
+
if (!normalizedType) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (pageIndex <= 1) {
|
|
40
|
+
return normalizedType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return PAGINATED_SCHEMA_TYPE_RULES[normalizedType] ?? normalizedType;
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
export class RenderEngine {
|
|
20
47
|
/**
|
|
21
48
|
* @type {TemplateRegistry}
|
|
@@ -181,7 +208,7 @@ export class RenderEngine {
|
|
|
181
208
|
* layout: string,
|
|
182
209
|
* template?: string,
|
|
183
210
|
* front: Record<string, unknown>,
|
|
184
|
-
* view
|
|
211
|
+
* view?: Record<string, unknown>,
|
|
185
212
|
* html: string,
|
|
186
213
|
* sourcePath?: string,
|
|
187
214
|
* outputPath?: string,
|
|
@@ -436,6 +463,14 @@ export class RenderEngine {
|
|
|
436
463
|
frontForPage.collectionType = collectionType;
|
|
437
464
|
}
|
|
438
465
|
|
|
466
|
+
const paginatedSchemaType = resolvePaginatedSchemaType(
|
|
467
|
+
frontForPage.schemaType,
|
|
468
|
+
pageIndex,
|
|
469
|
+
);
|
|
470
|
+
if (paginatedSchemaType) {
|
|
471
|
+
frontForPage.schemaType = paginatedSchemaType;
|
|
472
|
+
}
|
|
473
|
+
|
|
439
474
|
const renderedContent = await renderContentTemplate(
|
|
440
475
|
templateName,
|
|
441
476
|
contentHtml,
|
|
@@ -445,7 +480,11 @@ export class RenderEngine {
|
|
|
445
480
|
listing,
|
|
446
481
|
);
|
|
447
482
|
|
|
448
|
-
const pageMeta = metaEngine.buildPageMeta(
|
|
483
|
+
const pageMeta = await metaEngine.buildPageMeta(
|
|
484
|
+
frontForPage,
|
|
485
|
+
lang,
|
|
486
|
+
pageSlug,
|
|
487
|
+
);
|
|
449
488
|
const activeMenuKey = menuEngine.resolveActiveMenuKey(frontForPage);
|
|
450
489
|
const view = buildViewPayload({
|
|
451
490
|
lang,
|
|
@@ -687,7 +726,7 @@ export class RenderEngine {
|
|
|
687
726
|
lang,
|
|
688
727
|
dictionary,
|
|
689
728
|
);
|
|
690
|
-
const pageMeta = metaEngine.buildPageMeta(front, lang, slug);
|
|
729
|
+
const pageMeta = await metaEngine.buildPageMeta(front, lang, slug);
|
|
691
730
|
const layoutName = "default";
|
|
692
731
|
const activeMenuKey = menuEngine.resolveActiveMenuKey(front);
|
|
693
732
|
const view = buildViewPayload({
|
|
@@ -812,14 +851,7 @@ export class RenderEngine {
|
|
|
812
851
|
|
|
813
852
|
/** @param {Record<string, any> | { raw?: unknown } | null | undefined} front */
|
|
814
853
|
function normalizeFrontMatter(front) {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const raw =
|
|
820
|
-
"raw" in front && front.raw && typeof front.raw === "object"
|
|
821
|
-
? front.raw
|
|
822
|
-
: front;
|
|
823
|
-
|
|
824
|
-
return typeof raw === "object" && raw !== null ? { ...raw } : {};
|
|
854
|
+
const frontRecord = _fmt.toRecord(front);
|
|
855
|
+
const rawRecord = _fmt.pickFirstRecord(frontRecord?.raw, frontRecord);
|
|
856
|
+
return rawRecord ? { ...rawRecord } : {};
|
|
825
857
|
}
|
package/lib/contentFile.js
CHANGED
package/lib/contentHeader.js
CHANGED
|
@@ -104,6 +104,12 @@ export class ContentHeader {
|
|
|
104
104
|
: "";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
get schemaType() {
|
|
108
|
+
return typeof this._frontMatter.schemaType === "string"
|
|
109
|
+
? this._frontMatter.schemaType.trim().toLowerCase()
|
|
110
|
+
: "";
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
get related() {
|
|
108
114
|
return Array.isArray(this._frontMatter.related)
|
|
109
115
|
? this._frontMatter.related
|
|
@@ -124,9 +130,10 @@ export class ContentHeader {
|
|
|
124
130
|
|
|
125
131
|
get tags() {
|
|
126
132
|
const tags = _fmt.normalizeStringArray(this._frontMatter.tags);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
const normalized = tags
|
|
134
|
+
.map((tag) => _fmt.slugify(tag))
|
|
135
|
+
.filter((tag) => tag.length > 0);
|
|
136
|
+
return [...new Set(normalized)];
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
get keywords() {
|
|
@@ -154,6 +161,10 @@ export class ContentHeader {
|
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
get isPolicy() {
|
|
164
|
+
if (this.schemaType) {
|
|
165
|
+
return _fmt.boolean(this.schemaType === "policy");
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
const category =
|
|
158
169
|
typeof this._frontMatter.category === "string"
|
|
159
170
|
? this._frontMatter.category.trim()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shevky/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A minimal, dependency-light static site generator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "shevky.js",
|
|
@@ -36,12 +36,15 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://github.com/shevky/core#readme",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@shevky/base": "^0.0.
|
|
40
|
-
"@types/node": "^20.11.30",
|
|
41
|
-
"@types/mustache": "^4.2.6",
|
|
42
|
-
"@types/html-minifier-terser": "^7.0.2",
|
|
39
|
+
"@shevky/base": "^0.0.5",
|
|
43
40
|
"@tailwindcss/typography": "^0.5.19",
|
|
41
|
+
"@types/html-minifier-terser": "^7.0.2",
|
|
42
|
+
"@types/mustache": "^4.2.6",
|
|
43
|
+
"@types/node": "^20.11.30",
|
|
44
44
|
"autoprefixer": "^10.4.21",
|
|
45
|
+
"command-line-args": "^6.0.1",
|
|
46
|
+
"command-line-usage": "^7.0.3",
|
|
47
|
+
"degit": "^2.8.4",
|
|
45
48
|
"gray-matter": "^4.0.3",
|
|
46
49
|
"highlight.js": "^11.11.1",
|
|
47
50
|
"html-minifier-terser": "^6.1.0",
|
|
@@ -49,10 +52,7 @@
|
|
|
49
52
|
"marked-highlight": "^2.2.3",
|
|
50
53
|
"mustache": "^4.2.0",
|
|
51
54
|
"postcss": "^8.5.6",
|
|
52
|
-
"tailwindcss": "^4.1.14"
|
|
53
|
-
"command-line-args": "^6.0.1",
|
|
54
|
-
"command-line-usage": "^7.0.3",
|
|
55
|
-
"degit": "^2.8.4"
|
|
55
|
+
"tailwindcss": "^4.1.14"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=18"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { io as _io, config as _cfg } from "@shevky/base";
|
|
1
|
+
import { io as _io, config as _cfg, log as _log } from "@shevky/base";
|
|
2
2
|
import matter from "gray-matter";
|
|
3
3
|
|
|
4
4
|
import { ContentFile } from "../lib/contentFile.js";
|
|
@@ -37,6 +37,7 @@ export class ContentRegistry {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const files = await _io.directory.read(path);
|
|
40
|
+
let hasChanges = false;
|
|
40
41
|
for (const entry of files) {
|
|
41
42
|
if (!entry.endsWith(".md")) {
|
|
42
43
|
continue;
|
|
@@ -49,10 +50,14 @@ export class ContentRegistry {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const contentFile = await this.#_loadFromFile(filePath);
|
|
52
|
-
this.#
|
|
53
|
+
if (this.#_addUniqueContent(contentFile)) {
|
|
54
|
+
hasChanges = true;
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
if (hasChanges) {
|
|
59
|
+
this.#_resetCaches();
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
get count() {
|
|
@@ -72,8 +77,9 @@ export class ContentRegistry {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
if (input instanceof ContentFile) {
|
|
75
|
-
this.#
|
|
76
|
-
|
|
80
|
+
if (this.#_addUniqueContent(input)) {
|
|
81
|
+
this.#_resetCaches();
|
|
82
|
+
}
|
|
77
83
|
return;
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -92,8 +98,9 @@ export class ContentRegistry {
|
|
|
92
98
|
const isValid = typeof input.isValid === "boolean" ? input.isValid : true;
|
|
93
99
|
|
|
94
100
|
const contentFile = new ContentFile(header, content, sourcePath, isValid);
|
|
95
|
-
this.#
|
|
96
|
-
|
|
101
|
+
if (this.#_addUniqueContent(contentFile)) {
|
|
102
|
+
this.#_resetCaches();
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
@@ -117,7 +124,7 @@ export class ContentRegistry {
|
|
|
117
124
|
!file.isValid ||
|
|
118
125
|
file.isDraft ||
|
|
119
126
|
!file.isPublished ||
|
|
120
|
-
file.
|
|
127
|
+
file.schemaType !== "policy"
|
|
121
128
|
) {
|
|
122
129
|
continue;
|
|
123
130
|
}
|
|
@@ -325,4 +332,48 @@ export class ContentRegistry {
|
|
|
325
332
|
});
|
|
326
333
|
return sorted;
|
|
327
334
|
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Adds content only if required fields exist and id+lang is unique.
|
|
338
|
+
* @param {ContentFile} contentFile
|
|
339
|
+
*/
|
|
340
|
+
#_addUniqueContent(contentFile) {
|
|
341
|
+
if (!(contentFile instanceof ContentFile)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const id = typeof contentFile.id === "string" ? contentFile.id.trim() : "";
|
|
346
|
+
const lang =
|
|
347
|
+
typeof contentFile.lang === "string" ? contentFile.lang.trim() : "";
|
|
348
|
+
const sourcePath =
|
|
349
|
+
typeof contentFile.sourcePath === "string" &&
|
|
350
|
+
contentFile.sourcePath.trim().length > 0
|
|
351
|
+
? contentFile.sourcePath
|
|
352
|
+
: "unknown source";
|
|
353
|
+
|
|
354
|
+
/** @type {string[]} */
|
|
355
|
+
const missingFields = [];
|
|
356
|
+
if (!id) missingFields.push("id");
|
|
357
|
+
if (!lang) missingFields.push("lang");
|
|
358
|
+
if (missingFields.length > 0) {
|
|
359
|
+
_log.warn(
|
|
360
|
+
`[content] Skipped content: missing required field(s): ${missingFields.join(", ")} (${sourcePath})`,
|
|
361
|
+
);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const hasExisting = this.#_cache.some((entry) => {
|
|
366
|
+
const existingId = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
367
|
+
const existingLang =
|
|
368
|
+
typeof entry.lang === "string" ? entry.lang.trim() : "";
|
|
369
|
+
return existingId === id && existingLang === lang;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (hasExisting) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.#_cache.push(contentFile);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
328
379
|
}
|
package/scripts/build.js
CHANGED
|
@@ -86,6 +86,7 @@ const DEFAULT_IMAGE = _cfg.seo.defaultImage;
|
|
|
86
86
|
const FALLBACK_TAGLINES = { tr: "-", en: "-" };
|
|
87
87
|
/** @type {Record<string, any>} */
|
|
88
88
|
const COLLECTION_CONFIG = _cfg.content.collections;
|
|
89
|
+
const PAGE_BUFFER_LIMIT = resolvePageBufferLimit();
|
|
89
90
|
|
|
90
91
|
const GENERATED_PAGES = new Set();
|
|
91
92
|
|
|
@@ -145,16 +146,9 @@ function normalizeLogPath(pathValue) {
|
|
|
145
146
|
|
|
146
147
|
/** @param {FrontMatter | { raw?: unknown } | null | undefined} front */
|
|
147
148
|
function normalizeFrontMatter(front) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const raw =
|
|
153
|
-
"raw" in front && front.raw && typeof front.raw === "object"
|
|
154
|
-
? front.raw
|
|
155
|
-
: front;
|
|
156
|
-
|
|
157
|
-
return typeof raw === "object" && raw !== null ? { ...raw } : {};
|
|
149
|
+
const frontRecord = _fmt.toRecord(front);
|
|
150
|
+
const rawRecord = _fmt.pickFirstRecord(frontRecord?.raw, frontRecord);
|
|
151
|
+
return rawRecord ? { ...rawRecord } : {};
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
async function ensureDist() {
|
|
@@ -196,16 +190,14 @@ function injectAlternateLocaleMeta(html, locales) {
|
|
|
196
190
|
function resolvePaginationSegment(lang) {
|
|
197
191
|
/** @type {Record<string, string>} */
|
|
198
192
|
const segmentConfig = _cfg?.content?.pagination?.segment ?? {};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
) {
|
|
203
|
-
return segmentConfig[lang].trim();
|
|
193
|
+
const langSegment = _fmt.text(segmentConfig[lang]);
|
|
194
|
+
if (langSegment) {
|
|
195
|
+
return langSegment;
|
|
204
196
|
}
|
|
205
197
|
|
|
206
|
-
const defaultSegment = segmentConfig[_i18n.default];
|
|
207
|
-
if (
|
|
208
|
-
return defaultSegment
|
|
198
|
+
const defaultSegment = _fmt.text(segmentConfig[_i18n.default]);
|
|
199
|
+
if (defaultSegment) {
|
|
200
|
+
return defaultSegment;
|
|
209
201
|
}
|
|
210
202
|
|
|
211
203
|
return "page";
|
|
@@ -252,7 +244,17 @@ async function writeHtmlFile(relativePath, html, meta = {}) {
|
|
|
252
244
|
});
|
|
253
245
|
}
|
|
254
246
|
|
|
255
|
-
|
|
247
|
+
/** @param {{ force?: boolean }} [options] */
|
|
248
|
+
async function flushPages(options = {}) {
|
|
249
|
+
const force = _fmt.boolean(options.force);
|
|
250
|
+
if (pageRegistry.count === 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!force && pageRegistry.count < PAGE_BUFFER_LIMIT) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
256
258
|
const pages = pageRegistry
|
|
257
259
|
.list()
|
|
258
260
|
.sort((a, b) => (a.outputPath || "").localeCompare(b.outputPath || ""));
|
|
@@ -262,6 +264,7 @@ async function flushPages() {
|
|
|
262
264
|
}
|
|
263
265
|
await writeHtmlFile(page.outputPath, page.html, page.writeMeta ?? {});
|
|
264
266
|
}
|
|
267
|
+
pageRegistry.clear();
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
/** @param {string} html @param {string} langKey */
|
|
@@ -508,12 +511,12 @@ async function renderFragmentTemplate(
|
|
|
508
511
|
templateName,
|
|
509
512
|
listingOverride,
|
|
510
513
|
) {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
_log.warn(`[build] Fragment template not found: ${
|
|
514
|
+
const resolvedTemplateName = _fmt.text(templateName);
|
|
515
|
+
const template = resolvedTemplateName
|
|
516
|
+
? templateRegistry.getTemplate(TYPE_TEMPLATE, resolvedTemplateName)
|
|
517
|
+
: null;
|
|
518
|
+
if (resolvedTemplateName && !template) {
|
|
519
|
+
_log.warn(`[build] Fragment template not found: ${resolvedTemplateName}`);
|
|
517
520
|
}
|
|
518
521
|
const {
|
|
519
522
|
normalizedFront,
|
|
@@ -523,7 +526,7 @@ async function renderFragmentTemplate(
|
|
|
523
526
|
languageFlags,
|
|
524
527
|
resolvedDictionary,
|
|
525
528
|
} = buildContentRenderContext(front, lang, dictionary, listingOverride);
|
|
526
|
-
const decorated = decorateHtml(contentHtml,
|
|
529
|
+
const decorated = decorateHtml(contentHtml, resolvedTemplateName);
|
|
527
530
|
const templateContent = template?.content ?? "{{{content.html}}}";
|
|
528
531
|
|
|
529
532
|
return Mustache.render(
|
|
@@ -559,10 +562,7 @@ function buildContentRenderContext(front, lang, dictionary, listingOverride) {
|
|
|
559
562
|
const baseFront = normalizeFrontMatter(front);
|
|
560
563
|
/** @type {string[]} */
|
|
561
564
|
const normalizedTags = Array.isArray(front.tags)
|
|
562
|
-
? front.tags.filter(
|
|
563
|
-
(/** @type {string} */ tag) =>
|
|
564
|
-
typeof tag === "string" && tag.trim().length > 0,
|
|
565
|
-
)
|
|
565
|
+
? front.tags.filter((/** @type {string} */ tag) => _fmt.hasText(tag))
|
|
566
566
|
: [];
|
|
567
567
|
const tagLinks = normalizedTags
|
|
568
568
|
.map((/** @type {string} */ tag) => {
|
|
@@ -570,10 +570,8 @@ function buildContentRenderContext(front, lang, dictionary, listingOverride) {
|
|
|
570
570
|
return url ? { label: tag, url } : null;
|
|
571
571
|
})
|
|
572
572
|
.filter(Boolean);
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
? _fmt.slugify(front.category)
|
|
576
|
-
: "";
|
|
573
|
+
const categoryLabel = _fmt.text(front.category);
|
|
574
|
+
const categorySlug = categoryLabel ? _fmt.slugify(categoryLabel) : "";
|
|
577
575
|
const categoryUrl = categorySlug
|
|
578
576
|
? metaEngine.buildContentUrl(null, lang, categorySlug)
|
|
579
577
|
: null;
|
|
@@ -584,10 +582,7 @@ function buildContentRenderContext(front, lang, dictionary, listingOverride) {
|
|
|
584
582
|
tagLinks,
|
|
585
583
|
hasTags: normalizedTags.length > 0,
|
|
586
584
|
categoryUrl,
|
|
587
|
-
categoryLabel
|
|
588
|
-
typeof front.category === "string" && front.category.trim().length > 0
|
|
589
|
-
? front.category.trim()
|
|
590
|
-
: "",
|
|
585
|
+
categoryLabel,
|
|
591
586
|
dateDisplay: _fmt.date(front.date, lang),
|
|
592
587
|
updatedDisplay: _fmt.date(front.updated, lang),
|
|
593
588
|
cover: front.cover ?? DEFAULT_IMAGE,
|
|
@@ -633,28 +628,29 @@ async function renderPage({ layoutName, view, front, lang, slug, writeMeta }) {
|
|
|
633
628
|
minifyHtml,
|
|
634
629
|
});
|
|
635
630
|
const relativePath = buildOutputPath(front, lang, slug);
|
|
631
|
+
const templateName = _fmt.text(front?.template);
|
|
632
|
+
const pageType = _fmt.text(writeMeta?.type) || templateName;
|
|
633
|
+
const collectionType = normalizeCollectionTypeValue(front?.collectionType);
|
|
634
|
+
const slimFront = collectionType
|
|
635
|
+
? /** @type {FrontMatter} */ ({ collectionType })
|
|
636
|
+
: /** @type {FrontMatter} */ ({});
|
|
636
637
|
const page = renderEngine.createPage({
|
|
637
638
|
kind: "page",
|
|
638
|
-
type:
|
|
639
|
-
typeof writeMeta?.type === "string" && writeMeta.type.length > 0
|
|
640
|
-
? writeMeta.type
|
|
641
|
-
: typeof front?.template === "string"
|
|
642
|
-
? front.template
|
|
643
|
-
: "",
|
|
639
|
+
type: pageType,
|
|
644
640
|
lang,
|
|
645
641
|
slug,
|
|
646
642
|
canonical: metaEngine.buildContentUrl(front?.canonical, lang, slug),
|
|
647
643
|
layout: layoutName,
|
|
648
|
-
template:
|
|
649
|
-
front,
|
|
650
|
-
view,
|
|
644
|
+
template: templateName,
|
|
645
|
+
front: slimFront,
|
|
651
646
|
html: finalHtml,
|
|
652
|
-
sourcePath:
|
|
647
|
+
sourcePath: _fmt.text(writeMeta?.source),
|
|
653
648
|
outputPath: relativePath,
|
|
654
649
|
writeMeta,
|
|
655
650
|
});
|
|
656
651
|
GENERATED_PAGES.add(toPosixPath(relativePath));
|
|
657
652
|
registerLegacyPaths(lang, slug);
|
|
653
|
+
await flushPages();
|
|
658
654
|
return page;
|
|
659
655
|
}
|
|
660
656
|
|
|
@@ -698,6 +694,150 @@ function toPosixPath(value) {
|
|
|
698
694
|
return value.split(_io.path.separator).join("/");
|
|
699
695
|
}
|
|
700
696
|
|
|
697
|
+
/** @param {FrontMatter | { header?: FrontMatter } | null | undefined} input */
|
|
698
|
+
function resolveFrontMatterInput(input) {
|
|
699
|
+
const inputRecord = _fmt.toRecord(input);
|
|
700
|
+
const resolved = _fmt.pickFirstRecord(
|
|
701
|
+
inputRecord?.raw,
|
|
702
|
+
inputRecord?.header,
|
|
703
|
+
inputRecord,
|
|
704
|
+
);
|
|
705
|
+
return /** @type {FrontMatter} */ (resolved ?? {});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** @param {unknown} value */
|
|
709
|
+
function normalizeSchemaTypeValue(value) {
|
|
710
|
+
if (typeof value !== "string") {
|
|
711
|
+
return "";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return value.trim().toLowerCase();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @param {Record<string, any> | null | undefined} front
|
|
719
|
+
* @param {Record<string, any> | null | undefined} [derived]
|
|
720
|
+
*/
|
|
721
|
+
function resolveSchemaTypeForGeneration(front, derived) {
|
|
722
|
+
const frontSchemaType = normalizeSchemaTypeValue(front?.schemaType);
|
|
723
|
+
if (_plugin.isSchemaType(frontSchemaType)) {
|
|
724
|
+
return frontSchemaType;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const derivedSchemaType = normalizeSchemaTypeValue(derived?.schemaType);
|
|
728
|
+
if (_plugin.isSchemaType(derivedSchemaType)) {
|
|
729
|
+
return derivedSchemaType;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const collectionType = normalizeCollectionTypeValue(
|
|
733
|
+
front?.collectionType ?? derived?.collectionType,
|
|
734
|
+
);
|
|
735
|
+
if (collectionType) {
|
|
736
|
+
return "collection";
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return "page";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* @param {Record<string, any> | null | undefined} front
|
|
744
|
+
* @param {Record<string, any> | null | undefined} [derived]
|
|
745
|
+
*/
|
|
746
|
+
function injectSchemaTypeForGeneration(front, derived) {
|
|
747
|
+
if (!front || typeof front !== "object") {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const resolvedType = resolveSchemaTypeForGeneration(front, derived);
|
|
752
|
+
const currentFrontType = normalizeSchemaTypeValue(front.schemaType);
|
|
753
|
+
|
|
754
|
+
if (!_plugin.isSchemaType(currentFrontType)) {
|
|
755
|
+
try {
|
|
756
|
+
front.schemaType = resolvedType;
|
|
757
|
+
} catch {
|
|
758
|
+
// Ignore read-only objects.
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (derived && typeof derived === "object") {
|
|
763
|
+
const currentDerivedType = normalizeSchemaTypeValue(derived.schemaType);
|
|
764
|
+
if (!_plugin.isSchemaType(currentDerivedType)) {
|
|
765
|
+
try {
|
|
766
|
+
derived.schemaType = resolvedType;
|
|
767
|
+
} catch {
|
|
768
|
+
// Ignore read-only objects.
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** @param {FrontMatter} front @param {string} lang @param {string} slug */
|
|
775
|
+
function buildMinimalPageMeta(front, lang, slug) {
|
|
776
|
+
const canonical = metaEngine.resolveUrl(
|
|
777
|
+
_fmt.text(front?.canonical) || metaEngine.buildContentUrl(null, lang, slug),
|
|
778
|
+
);
|
|
779
|
+
const alternates = metaEngine.buildAlternateUrlMap(front, lang, canonical);
|
|
780
|
+
const alternateLinks = metaEngine.buildAlternateLinkList(alternates);
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
title: _fmt.text(front?.metaTitle) || _fmt.text(front?.title),
|
|
784
|
+
description: _fmt.text(front?.description),
|
|
785
|
+
robots: _fmt.text(front?.robots) || "index,follow",
|
|
786
|
+
canonical,
|
|
787
|
+
alternates,
|
|
788
|
+
alternateLinks,
|
|
789
|
+
og: _fmt.toRecord(front?.og, {}),
|
|
790
|
+
twitter: _fmt.toRecord(front?.twitter, {}),
|
|
791
|
+
structuredData: front?.structuredData ?? "",
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** @param {FrontMatter | { header?: FrontMatter } | null | undefined} input @param {string} lang @param {string} slug @param {Record<string, any> | null | undefined} [derived] */
|
|
796
|
+
async function buildPageMetaWithPlugins(input, lang, slug, derived) {
|
|
797
|
+
const front = resolveFrontMatterInput(input);
|
|
798
|
+
const derivedFront =
|
|
799
|
+
derived && typeof derived === "object"
|
|
800
|
+
? /** @type {Record<string, any>} */ (derived)
|
|
801
|
+
: front;
|
|
802
|
+
injectSchemaTypeForGeneration(front, derivedFront);
|
|
803
|
+
let pluginPageMeta = null;
|
|
804
|
+
|
|
805
|
+
pluginEngine.setRuntimeContext({
|
|
806
|
+
frontMatter: front,
|
|
807
|
+
derivedFrontMatter: derivedFront,
|
|
808
|
+
lang,
|
|
809
|
+
slug,
|
|
810
|
+
setPageMeta: (/** @type {Record<string, any>} */ meta) => {
|
|
811
|
+
pluginPageMeta = meta;
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
await pluginEngine.execute(_plugin.hooks.PAGE_META);
|
|
817
|
+
} finally {
|
|
818
|
+
pluginEngine.clearRuntimeContext();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (
|
|
822
|
+
pluginPageMeta &&
|
|
823
|
+
typeof pluginPageMeta === "object" &&
|
|
824
|
+
!Array.isArray(pluginPageMeta)
|
|
825
|
+
) {
|
|
826
|
+
return /** @type {Record<string, any>} */ (pluginPageMeta);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const existingPageMeta = _fmt.toRecord(front?.pageMeta);
|
|
830
|
+
if (existingPageMeta) {
|
|
831
|
+
return existingPageMeta;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
_log.debug(
|
|
835
|
+
`[build] Missing page meta from '${_plugin.hooks.PAGE_META}' hook for lang='${lang}' slug='${slug}'. Using front matter as page meta.`,
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
return buildMinimalPageMeta(front, lang, slug);
|
|
839
|
+
}
|
|
840
|
+
|
|
701
841
|
/** @param {FrontMatter} front @param {string} lang */
|
|
702
842
|
function buildCollectionListing(front, lang) {
|
|
703
843
|
const normalizedLang = lang ?? _i18n.default;
|
|
@@ -724,19 +864,13 @@ function buildCollectionListing(front, lang) {
|
|
|
724
864
|
function buildSeriesListing(front, lang) {
|
|
725
865
|
/** @type {string[]} */
|
|
726
866
|
const relatedSource = Array.isArray(front?.related) ? front.related : [];
|
|
727
|
-
const seriesName =
|
|
728
|
-
|
|
729
|
-
front.seriesTitle.trim().length > 0
|
|
730
|
-
? front.seriesTitle.trim()
|
|
731
|
-
: typeof front?.series === "string"
|
|
732
|
-
? front.series.trim()
|
|
733
|
-
: "";
|
|
734
|
-
const currentId = typeof front?.id === "string" ? front.id.trim() : "";
|
|
867
|
+
const seriesName = _fmt.text(front?.seriesTitle) || _fmt.text(front?.series);
|
|
868
|
+
const currentId = _fmt.text(front?.id);
|
|
735
869
|
/** @type {Array<{ id: string, label: string, url: string, hasUrl?: boolean, isCurrent: boolean, isPlaceholder: boolean }>} */
|
|
736
870
|
const items = [];
|
|
737
871
|
|
|
738
872
|
relatedSource.forEach((/** @type {string} */ entry) => {
|
|
739
|
-
const value =
|
|
873
|
+
const value = _fmt.text(entry);
|
|
740
874
|
if (!value) {
|
|
741
875
|
items.push({
|
|
742
876
|
id: "",
|
|
@@ -804,19 +938,16 @@ function resolveCollectionType(front, items, fallback) {
|
|
|
804
938
|
}
|
|
805
939
|
|
|
806
940
|
if (Array.isArray(items)) {
|
|
807
|
-
const entryWithType = items.find(
|
|
808
|
-
|
|
809
|
-
typeof entry?.type === "string" && entry.type.trim().length > 0,
|
|
810
|
-
);
|
|
811
|
-
const entryType =
|
|
812
|
-
typeof entryWithType?.type === "string" ? entryWithType.type.trim() : "";
|
|
941
|
+
const entryWithType = items.find((entry) => _fmt.hasText(entry?.type));
|
|
942
|
+
const entryType = _fmt.text(entryWithType?.type);
|
|
813
943
|
if (entryType) {
|
|
814
944
|
return entryType.toLowerCase();
|
|
815
945
|
}
|
|
816
946
|
}
|
|
817
947
|
|
|
818
|
-
|
|
819
|
-
|
|
948
|
+
const normalizedFallback = _fmt.text(fallback);
|
|
949
|
+
if (normalizedFallback) {
|
|
950
|
+
return normalizedFallback.toLowerCase();
|
|
820
951
|
}
|
|
821
952
|
|
|
822
953
|
return "";
|
|
@@ -858,20 +989,23 @@ function resolveListingKey(front) {
|
|
|
858
989
|
function resolveListingEmpty(front, lang) {
|
|
859
990
|
if (!front) return "";
|
|
860
991
|
const { listingEmpty } = front;
|
|
861
|
-
|
|
862
|
-
|
|
992
|
+
const direct = _fmt.text(listingEmpty);
|
|
993
|
+
if (direct) {
|
|
994
|
+
return direct;
|
|
863
995
|
}
|
|
864
996
|
if (listingEmpty && typeof listingEmpty === "object") {
|
|
865
997
|
const listingEmptyMap = /** @type {Record<string, string>} */ (
|
|
866
998
|
listingEmpty
|
|
867
999
|
);
|
|
868
1000
|
const localized = listingEmptyMap[lang];
|
|
869
|
-
|
|
870
|
-
|
|
1001
|
+
const localizedValue = _fmt.text(localized);
|
|
1002
|
+
if (localizedValue) {
|
|
1003
|
+
return localizedValue;
|
|
871
1004
|
}
|
|
872
1005
|
const fallback = listingEmptyMap[_i18n.default];
|
|
873
|
-
|
|
874
|
-
|
|
1006
|
+
const fallbackValue = _fmt.text(fallback);
|
|
1007
|
+
if (fallbackValue) {
|
|
1008
|
+
return fallbackValue;
|
|
875
1009
|
}
|
|
876
1010
|
}
|
|
877
1011
|
return "";
|
|
@@ -880,16 +1014,95 @@ function resolveListingEmpty(front, lang) {
|
|
|
880
1014
|
/** @param {FrontMatter} front */
|
|
881
1015
|
function resolveListingHeading(front) {
|
|
882
1016
|
if (!front) return "";
|
|
1017
|
+
return _fmt.text(front.listHeading) || _fmt.text(front.title);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function resolvePageBufferLimit() {
|
|
1021
|
+
const envValue = Number.parseInt(
|
|
1022
|
+
process.env.SHEVKY_PAGE_BUFFER_LIMIT ?? "",
|
|
1023
|
+
10,
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
1027
|
+
return envValue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const configValue = Number.parseInt(
|
|
1031
|
+
String(_cfg?.build?.pageBufferLimit ?? ""),
|
|
1032
|
+
10,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (Number.isFinite(configValue) && configValue > 0) {
|
|
1036
|
+
return configValue;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return 20;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** @param {unknown} value */
|
|
1043
|
+
function resolveAliasOutputPath(value) {
|
|
1044
|
+
const raw = _fmt.text(value);
|
|
1045
|
+
if (!raw) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const relative = metaEngine.canonicalToRelativePath(raw);
|
|
1050
|
+
if (relative) {
|
|
1051
|
+
const lastSegment = relative.split("/").pop()?.trim() ?? "";
|
|
1052
|
+
if (lastSegment.includes(".")) {
|
|
1053
|
+
return relative;
|
|
1054
|
+
}
|
|
1055
|
+
return _io.path.combine(relative, "index.html");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const normalizedRaw = raw.trim();
|
|
883
1059
|
if (
|
|
884
|
-
|
|
885
|
-
|
|
1060
|
+
normalizedRaw === "/" ||
|
|
1061
|
+
normalizedRaw === "~/" ||
|
|
1062
|
+
/^https?:\/\/[^/]+\/?$/i.test(normalizedRaw)
|
|
886
1063
|
) {
|
|
887
|
-
return
|
|
1064
|
+
return "index.html";
|
|
888
1065
|
}
|
|
889
|
-
|
|
890
|
-
|
|
1066
|
+
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function applyOutputAliases() {
|
|
1071
|
+
const aliases = Array.isArray(_cfg?.build?.outputAliases)
|
|
1072
|
+
? _cfg.build.outputAliases
|
|
1073
|
+
: [];
|
|
1074
|
+
|
|
1075
|
+
for (const entry of aliases) {
|
|
1076
|
+
const alias = _fmt.toRecord(entry);
|
|
1077
|
+
if (!alias) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const sourceRelative = resolveAliasOutputPath(alias.from);
|
|
1082
|
+
const targetRelative = resolveAliasOutputPath(alias.to);
|
|
1083
|
+
if (!sourceRelative || !targetRelative || sourceRelative === targetRelative) {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const sourcePath = _io.path.combine(DIST_DIR, sourceRelative);
|
|
1088
|
+
if (!(await _io.file.exists(sourcePath))) {
|
|
1089
|
+
_log.warn(
|
|
1090
|
+
`[build] Output alias source not found: ${normalizeLogPath(sourcePath)}`,
|
|
1091
|
+
);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const targetPath = _io.path.combine(DIST_DIR, targetRelative);
|
|
1096
|
+
await _io.directory.create(_io.path.name(targetPath));
|
|
1097
|
+
await _io.file.copy(sourcePath, targetPath);
|
|
1098
|
+
GENERATED_PAGES.add(toPosixPath(targetRelative));
|
|
1099
|
+
|
|
1100
|
+
_log.step("COPY_ALIAS", {
|
|
1101
|
+
source: normalizeLogPath(sourcePath),
|
|
1102
|
+
target: normalizeLogPath(targetPath),
|
|
1103
|
+
type: "alias",
|
|
1104
|
+
});
|
|
891
1105
|
}
|
|
892
|
-
return "";
|
|
893
1106
|
}
|
|
894
1107
|
|
|
895
1108
|
async function buildContentPages() {
|
|
@@ -932,10 +1145,7 @@ async function buildContentPages() {
|
|
|
932
1145
|
const rawFront = normalizeFrontMatter(file.header);
|
|
933
1146
|
const shouldBuildFragment = _fmt.boolean(rawFront.fragment);
|
|
934
1147
|
const fragmentTemplateName =
|
|
935
|
-
|
|
936
|
-
rawFront.fragmentTemplate.trim().length > 0
|
|
937
|
-
? rawFront.fragmentTemplate.trim()
|
|
938
|
-
: file.template;
|
|
1148
|
+
_fmt.text(rawFront.fragmentTemplate) || file.template;
|
|
939
1149
|
|
|
940
1150
|
if (file.template === "collection" || file.template === "home") {
|
|
941
1151
|
await renderEngine.buildPaginatedCollectionPages({
|
|
@@ -960,7 +1170,15 @@ async function buildContentPages() {
|
|
|
960
1170
|
buildEasterEggPayload,
|
|
961
1171
|
}),
|
|
962
1172
|
renderPage,
|
|
963
|
-
metaEngine
|
|
1173
|
+
metaEngine: {
|
|
1174
|
+
buildPageMeta: async (frontForPage, pageLang, pageSlug) =>
|
|
1175
|
+
buildPageMetaWithPlugins(
|
|
1176
|
+
frontForPage,
|
|
1177
|
+
pageLang,
|
|
1178
|
+
pageSlug,
|
|
1179
|
+
frontForPage,
|
|
1180
|
+
),
|
|
1181
|
+
},
|
|
964
1182
|
menuEngine,
|
|
965
1183
|
resolveListingKey,
|
|
966
1184
|
resolveListingEmpty,
|
|
@@ -981,10 +1199,11 @@ async function buildContentPages() {
|
|
|
981
1199
|
file.lang,
|
|
982
1200
|
dictionary,
|
|
983
1201
|
);
|
|
984
|
-
const pageMeta =
|
|
1202
|
+
const pageMeta = await buildPageMetaWithPlugins(
|
|
985
1203
|
file.header,
|
|
986
1204
|
file.lang,
|
|
987
1205
|
file.slug,
|
|
1206
|
+
file,
|
|
988
1207
|
);
|
|
989
1208
|
const activeMenuKey = menuEngine.resolveActiveMenuKey(file.header);
|
|
990
1209
|
const view = renderEngine.buildViewPayload(
|
|
@@ -1043,10 +1262,7 @@ async function buildContentPages() {
|
|
|
1043
1262
|
minifyHtml,
|
|
1044
1263
|
},
|
|
1045
1264
|
);
|
|
1046
|
-
const fragmentLang =
|
|
1047
|
-
typeof file.lang === "string" && file.lang.trim().length > 0
|
|
1048
|
-
? file.lang.trim()
|
|
1049
|
-
: _i18n.default;
|
|
1265
|
+
const fragmentLang = _fmt.text(file.lang) || _i18n.default;
|
|
1050
1266
|
const fragmentPath = _io.path.combine(
|
|
1051
1267
|
FRAGMENTS_DIR,
|
|
1052
1268
|
fragmentLang,
|
|
@@ -1081,7 +1297,15 @@ async function buildContentPages() {
|
|
|
1081
1297
|
buildEasterEggPayload,
|
|
1082
1298
|
}),
|
|
1083
1299
|
renderPage,
|
|
1084
|
-
metaEngine
|
|
1300
|
+
metaEngine: {
|
|
1301
|
+
buildPageMeta: async (frontForPage, pageLang, pageSlug) =>
|
|
1302
|
+
buildPageMetaWithPlugins(
|
|
1303
|
+
frontForPage,
|
|
1304
|
+
pageLang,
|
|
1305
|
+
pageSlug,
|
|
1306
|
+
frontForPage,
|
|
1307
|
+
),
|
|
1308
|
+
},
|
|
1085
1309
|
menuEngine,
|
|
1086
1310
|
resolveCollectionType,
|
|
1087
1311
|
normalizeCollectionTypeValue,
|
|
@@ -1097,15 +1321,9 @@ async function buildContentPages() {
|
|
|
1097
1321
|
function resolveCollectionDisplayKey(configKey, defaultKey, items) {
|
|
1098
1322
|
if (configKey === "series" && Array.isArray(items)) {
|
|
1099
1323
|
const entryWithTitle = items.find(
|
|
1100
|
-
(entry) =>
|
|
1101
|
-
entry &&
|
|
1102
|
-
typeof entry.seriesTitle === "string" &&
|
|
1103
|
-
entry.seriesTitle.trim().length > 0,
|
|
1324
|
+
(entry) => entry && _fmt.hasText(entry.seriesTitle),
|
|
1104
1325
|
);
|
|
1105
|
-
const seriesTitle =
|
|
1106
|
-
typeof entryWithTitle?.seriesTitle === "string"
|
|
1107
|
-
? entryWithTitle.seriesTitle.trim()
|
|
1108
|
-
: "";
|
|
1326
|
+
const seriesTitle = _fmt.text(entryWithTitle?.seriesTitle);
|
|
1109
1327
|
if (seriesTitle) {
|
|
1110
1328
|
return seriesTitle;
|
|
1111
1329
|
}
|
|
@@ -1162,6 +1380,14 @@ async function copyHtmlRecursive(currentDir = SRC_DIR, relative = "") {
|
|
|
1162
1380
|
for (const entry of entries) {
|
|
1163
1381
|
const fullPath = _io.path.combine(currentDir, entry);
|
|
1164
1382
|
const relPath = relative ? _io.path.combine(relative, entry) : entry;
|
|
1383
|
+
const normalizedRelPath = toPosixPath(relPath);
|
|
1384
|
+
|
|
1385
|
+
if (
|
|
1386
|
+
normalizedRelPath === "content" ||
|
|
1387
|
+
normalizedRelPath.startsWith("content/")
|
|
1388
|
+
) {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1165
1391
|
|
|
1166
1392
|
if (!entry.endsWith(".html")) {
|
|
1167
1393
|
continue;
|
|
@@ -1209,6 +1435,83 @@ async function copyHtmlRecursive(currentDir = SRC_DIR, relative = "") {
|
|
|
1209
1435
|
}
|
|
1210
1436
|
}
|
|
1211
1437
|
|
|
1438
|
+
/** @param {string} currentDir */
|
|
1439
|
+
async function copyContentStaticRecursive(currentDir = CONTENT_DIR) {
|
|
1440
|
+
if (!(await _io.directory.exists(currentDir))) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const contentRootDirectories = resolveContentRootDirectories();
|
|
1445
|
+
const entries = await _io.directory.read(currentDir);
|
|
1446
|
+
for (const entry of entries) {
|
|
1447
|
+
const fullPath = _io.path.combine(currentDir, entry);
|
|
1448
|
+
const relPath = entry;
|
|
1449
|
+
const normalizedRelPath = toPosixPath(relPath);
|
|
1450
|
+
const isXml = entry.endsWith(".xml");
|
|
1451
|
+
|
|
1452
|
+
if (
|
|
1453
|
+
contentRootDirectories.some(
|
|
1454
|
+
(directory) =>
|
|
1455
|
+
normalizedRelPath === directory ||
|
|
1456
|
+
normalizedRelPath.startsWith(`${directory}/`),
|
|
1457
|
+
)
|
|
1458
|
+
) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (!(entry.endsWith(".html") || entry.endsWith(".xml"))) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (GENERATED_PAGES.has(toPosixPath(relPath))) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const raw = await _io.file.read(fullPath);
|
|
1471
|
+
const transformed = isXml
|
|
1472
|
+
? raw
|
|
1473
|
+
: await renderEngine.transformHtml(raw, {
|
|
1474
|
+
versionToken,
|
|
1475
|
+
minifyHtml,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
await writeHtmlFile(relPath, transformed, {
|
|
1479
|
+
action: "COPY_HTML",
|
|
1480
|
+
type: isXml ? "content-xml" : "content-static",
|
|
1481
|
+
source: fullPath,
|
|
1482
|
+
lang: _i18n.default,
|
|
1483
|
+
inputBytes: byteLength(transformed),
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function resolveContentRootDirectories() {
|
|
1489
|
+
const configured = Array.isArray(_cfg?.build?.contentRootDirectories)
|
|
1490
|
+
? _cfg.build.contentRootDirectories
|
|
1491
|
+
: [".well-known"];
|
|
1492
|
+
|
|
1493
|
+
return [...new Set(configured.map((entry) => _fmt.text(entry)).filter(Boolean))];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async function copyContentRootDirectories() {
|
|
1497
|
+
const directories = resolveContentRootDirectories();
|
|
1498
|
+
for (const directory of directories) {
|
|
1499
|
+
const sourceDir = _io.path.combine(CONTENT_DIR, directory);
|
|
1500
|
+
if (!(await _io.directory.exists(sourceDir))) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const targetDir = _io.path.combine(DIST_DIR, directory);
|
|
1505
|
+
await _io.directory.copy(sourceDir, targetDir);
|
|
1506
|
+
|
|
1507
|
+
_log.step("COPY_DIR", {
|
|
1508
|
+
source: normalizeLogPath(sourceDir),
|
|
1509
|
+
target: normalizeLogPath(targetDir),
|
|
1510
|
+
type: "content-root-directory",
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1212
1515
|
async function copyStaticAssets() {
|
|
1213
1516
|
if (!(await _io.directory.exists(ASSETS_DIR))) {
|
|
1214
1517
|
return;
|
|
@@ -1243,7 +1546,10 @@ async function main() {
|
|
|
1243
1546
|
|
|
1244
1547
|
await buildContentPages();
|
|
1245
1548
|
await copyHtmlRecursive();
|
|
1246
|
-
await
|
|
1549
|
+
await copyContentStaticRecursive();
|
|
1550
|
+
await copyContentRootDirectories();
|
|
1551
|
+
await flushPages({ force: true });
|
|
1552
|
+
await applyOutputAliases();
|
|
1247
1553
|
}
|
|
1248
1554
|
|
|
1249
1555
|
const API = {
|
package/scripts/main.js
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface PluginExecutionContext extends BasePluginContext {
|
|
|
55
55
|
Record<string, { id: string; lang: string; title: string; canonical: string }>
|
|
56
56
|
>;
|
|
57
57
|
footerPolicies?: Record<string, FooterPolicy[]>;
|
|
58
|
+
i18n?: Record<string, any>;
|
|
59
|
+
frontMatter?: FrontMatter;
|
|
60
|
+
derivedFrontMatter?: FrontMatter | Record<string, any>;
|
|
61
|
+
lang?: string;
|
|
62
|
+
slug?: string;
|
|
63
|
+
pageMeta?: Record<string, any> | null;
|
|
64
|
+
setPageMeta?: (meta: Record<string, any>) => void;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export type Placeholder = {
|