@rettangoli/sites 1.0.2 → 1.0.3
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 +8 -1
- package/package.json +1 -1
- package/src/cli/build.js +1 -0
- package/src/createSiteBuilder.js +27 -17
- package/src/utils/loadSiteConfig.js +25 -2
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ my-site/
|
|
|
33
33
|
- YAML pages rendered through `jempl` + `yahtml`
|
|
34
34
|
- Markdown pages rendered through `markdown-it` + Shiki (default `rtglMarkdown`)
|
|
35
35
|
- Frontmatter (`template`, `tags`, arbitrary page metadata)
|
|
36
|
-
- Global data
|
|
36
|
+
- Global data from `data/*.yaml` and optional inline `sites.config.yaml data`
|
|
37
37
|
- Collections built from page tags
|
|
38
38
|
- `$if`, `$for`, `$partial`, template functions
|
|
39
39
|
- Static file copying from `static/` to `_site/`
|
|
@@ -75,6 +75,9 @@ imports:
|
|
|
75
75
|
docs: https://example.com/templates/docs.yaml
|
|
76
76
|
partials:
|
|
77
77
|
docs/nav: https://example.com/partials/docs-nav.yaml
|
|
78
|
+
data:
|
|
79
|
+
themeCssHref: /public/theme.css
|
|
80
|
+
themeBodyClass: dark
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
In the default starter template, CDN runtime scripts are controlled via `data/site.yaml`:
|
|
@@ -97,6 +100,10 @@ Example mappings:
|
|
|
97
100
|
- page frontmatter: `template: base` or `template: docs`
|
|
98
101
|
- template/page content: `$partial: docs/nav`
|
|
99
102
|
|
|
103
|
+
Use top-level `data` in `sites.config.yaml` for small global values that do not deserve their own `data/*.yaml` file.
|
|
104
|
+
`sites.config.yaml data` and `data/*.yaml` are merged, with `data/*.yaml` winning on conflicts.
|
|
105
|
+
Inline config data requires `rtgl >= 1.1.4` or `@rettangoli/sites >= 1.0.3`.
|
|
106
|
+
|
|
100
107
|
Imported files are cached on disk under `.rettangoli/sites/imports/{templates|partials}/` (hashed filenames).
|
|
101
108
|
Alias/url/hash mapping is tracked in `.rettangoli/sites/imports/index.yaml`.
|
|
102
109
|
Build is cache-first: if a cached file exists, it is used without a network request.
|
package/package.json
CHANGED
package/src/cli/build.js
CHANGED
|
@@ -31,6 +31,7 @@ export const buildSite = async (options = {}) => {
|
|
|
31
31
|
markdown: config.markdown || {},
|
|
32
32
|
keepMarkdownFiles: config.build?.keepMarkdownFiles === true,
|
|
33
33
|
imports: config.imports || {},
|
|
34
|
+
data: config.data || {},
|
|
34
35
|
functions: functions || {},
|
|
35
36
|
quiet,
|
|
36
37
|
isScreenshotMode
|
package/src/createSiteBuilder.js
CHANGED
|
@@ -17,22 +17,25 @@ const MATTER_OPTIONS = {
|
|
|
17
17
|
|
|
18
18
|
// Deep merge utility function
|
|
19
19
|
function deepMerge(target, source) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Object.assign(output, { [key]: source[key] });
|
|
27
|
-
} else {
|
|
28
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
Object.assign(output, { [key]: source[key] });
|
|
32
|
-
}
|
|
33
|
-
});
|
|
20
|
+
if (!isObject(source)) {
|
|
21
|
+
return source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!isObject(target)) {
|
|
25
|
+
return { ...source };
|
|
34
26
|
}
|
|
35
|
-
|
|
27
|
+
|
|
28
|
+
const output = { ...target };
|
|
29
|
+
|
|
30
|
+
Object.keys(source).forEach(key => {
|
|
31
|
+
if (isObject(source[key]) && isObject(target[key])) {
|
|
32
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Object.assign(output, { [key]: source[key] });
|
|
37
|
+
});
|
|
38
|
+
|
|
36
39
|
return output;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -314,6 +317,7 @@ export function createSiteBuilder({
|
|
|
314
317
|
markdown = {},
|
|
315
318
|
keepMarkdownFiles = false,
|
|
316
319
|
imports = {},
|
|
320
|
+
data = {},
|
|
317
321
|
fetchImpl,
|
|
318
322
|
functions = {},
|
|
319
323
|
quiet = false,
|
|
@@ -409,9 +413,13 @@ export function createSiteBuilder({
|
|
|
409
413
|
readPartialsRecursively(partialsDir);
|
|
410
414
|
}
|
|
411
415
|
|
|
416
|
+
if (!isObject(data)) {
|
|
417
|
+
throw new Error('Invalid site data: expected an object.');
|
|
418
|
+
}
|
|
419
|
+
|
|
412
420
|
// Read all data files and create a JSON object
|
|
413
421
|
const dataDir = path.join(rootDir, 'data');
|
|
414
|
-
const
|
|
422
|
+
const fileData = {};
|
|
415
423
|
|
|
416
424
|
if (fs.existsSync(dataDir)) {
|
|
417
425
|
const files = fs.readdirSync(dataDir);
|
|
@@ -421,11 +429,13 @@ export function createSiteBuilder({
|
|
|
421
429
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
422
430
|
const nameWithoutExt = path.basename(file, path.extname(file));
|
|
423
431
|
// Load YAML content and store under filename key
|
|
424
|
-
|
|
432
|
+
fileData[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
|
|
425
433
|
}
|
|
426
434
|
});
|
|
427
435
|
}
|
|
428
436
|
|
|
437
|
+
const globalData = deepMerge(data, fileData);
|
|
438
|
+
|
|
429
439
|
// Read all templates and create a JSON object
|
|
430
440
|
const templatesDir = path.join(rootDir, 'templates');
|
|
431
441
|
const templates = { ...importedTemplates };
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
|
|
5
|
-
const ALLOWED_TOP_LEVEL_KEYS = new Set(['markdown', 'markdownit', 'build', 'imports']);
|
|
5
|
+
const ALLOWED_TOP_LEVEL_KEYS = new Set(['markdown', 'markdownit', 'build', 'imports', 'data']);
|
|
6
6
|
const MARKDOWN_BOOLEAN_KEYS = new Set(['html', 'linkify', 'typographer', 'breaks', 'xhtmlOut']);
|
|
7
7
|
const MARKDOWN_STRING_KEYS = new Set(['langPrefix', 'quotes', 'preset']);
|
|
8
8
|
const MARKDOWN_NUMBER_KEYS = new Set(['maxNesting']);
|
|
@@ -121,6 +121,25 @@ function validateBuildConfig(value, configPath) {
|
|
|
121
121
|
return { ...value };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
function validateDataConfig(value, configPath) {
|
|
125
|
+
if (!isPlainObject(value)) {
|
|
126
|
+
throw new Error(`Invalid data config in "${configPath}": expected an object.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalized = {};
|
|
130
|
+
|
|
131
|
+
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
132
|
+
const key = String(rawKey).trim();
|
|
133
|
+
if (key === '') {
|
|
134
|
+
throw new Error(`Invalid data config in "${configPath}": keys must be non-empty.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
normalized[key] = rawValue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
function validateImportUrl(url, configPath, groupKey, alias) {
|
|
125
144
|
if (typeof url !== 'string' || url.trim() === '') {
|
|
126
145
|
throw new Error(`Invalid imports.${groupKey} value for alias "${alias}" in "${configPath}": expected a non-empty URL string.`);
|
|
@@ -200,7 +219,7 @@ function validateConfig(rawConfig, configPath) {
|
|
|
200
219
|
for (const key of Object.keys(config)) {
|
|
201
220
|
if (!ALLOWED_TOP_LEVEL_KEYS.has(key)) {
|
|
202
221
|
throw new Error(
|
|
203
|
-
`Unsupported key "${key}" in "${configPath}". Supported keys: markdownit (recommended), markdown (legacy alias), build, imports.`
|
|
222
|
+
`Unsupported key "${key}" in "${configPath}". Supported keys: markdownit (recommended), markdown (legacy alias), build, imports, data.`
|
|
204
223
|
);
|
|
205
224
|
}
|
|
206
225
|
}
|
|
@@ -292,6 +311,10 @@ function validateConfig(rawConfig, configPath) {
|
|
|
292
311
|
normalizedConfig.imports = validateImportsConfig(config.imports, configPath);
|
|
293
312
|
}
|
|
294
313
|
|
|
314
|
+
if (config.data !== undefined) {
|
|
315
|
+
normalizedConfig.data = validateDataConfig(config.data, configPath);
|
|
316
|
+
}
|
|
317
|
+
|
|
295
318
|
return normalizedConfig;
|
|
296
319
|
}
|
|
297
320
|
|