@rettangoli/sites 1.0.1 → 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 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 (`data/*.yaml`) merged with page frontmatter
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.
@@ -210,7 +217,7 @@ Use VT against your generated site:
210
217
  Docker runtime (recommended for stable Playwright/browser versions):
211
218
 
212
219
  ```bash
213
- IMAGE="han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.5"
220
+ IMAGE="han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0"
214
221
  docker pull "$IMAGE"
215
222
  docker run --rm -v "$PWD:/workspace" -w /workspace "$IMAGE" rtgl vt screenshot
216
223
  docker run --rm -v "$PWD:/workspace" -w /workspace "$IMAGE" rtgl vt report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/sites",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Generate static sites using Markdown and YAML for docs, blogs, and marketing sites.",
5
5
  "author": {
6
6
  "name": "Luciano Hanyon Wu",
@@ -177,6 +177,10 @@ function mdImpl(content) {
177
177
  };
178
178
  }
179
179
 
180
+ function defaultImpl(value, fallbackValue = '') {
181
+ return value == null || value === '' ? fallbackValue : value;
182
+ }
183
+
180
184
  export const builtinTemplateFunctions = {
181
185
  encodeURI: (value) => encodeURI(String(value ?? '')),
182
186
  encodeURIComponent: (value) => encodeURIComponent(String(value ?? '')),
@@ -188,6 +192,7 @@ export const builtinTemplateFunctions = {
188
192
  sort: sortImpl,
189
193
  chunk: chunkImpl,
190
194
  md: mdImpl,
195
+ default: defaultImpl,
191
196
  toQueryString,
192
197
  };
193
198
 
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
@@ -17,22 +17,25 @@ const MATTER_OPTIONS = {
17
17
 
18
18
  // Deep merge utility function
19
19
  function deepMerge(target, source) {
20
- const output = { ...target };
21
-
22
- if (isObject(target) && isObject(source)) {
23
- Object.keys(source).forEach(key => {
24
- if (isObject(source[key])) {
25
- if (!(key in target)) {
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 globalData = {};
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
- globalData[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
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