@stati/core 1.27.2 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/config/errors.d.ts +10 -0
  2. package/dist/config/errors.d.ts.map +1 -0
  3. package/dist/config/errors.js +10 -0
  4. package/dist/config/loader.d.ts.map +1 -1
  5. package/dist/config/loader.js +15 -2
  6. package/dist/constants.d.ts +26 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +23 -0
  9. package/dist/core/build.d.ts.map +1 -1
  10. package/dist/core/build.js +96 -24
  11. package/dist/core/isg/builder.d.ts +14 -2
  12. package/dist/core/isg/builder.d.ts.map +1 -1
  13. package/dist/core/isg/builder.js +41 -7
  14. package/dist/core/isg/deps.d.ts +9 -0
  15. package/dist/core/isg/deps.d.ts.map +1 -1
  16. package/dist/core/isg/deps.js +30 -1
  17. package/dist/core/isg/hash.d.ts +10 -0
  18. package/dist/core/isg/hash.d.ts.map +1 -1
  19. package/dist/core/isg/hash.js +14 -0
  20. package/dist/core/isg/index.d.ts +2 -1
  21. package/dist/core/isg/index.d.ts.map +1 -1
  22. package/dist/core/isg/index.js +1 -1
  23. package/dist/core/isg/manifest.js +8 -0
  24. package/dist/core/markdown.d.ts +10 -1
  25. package/dist/core/markdown.d.ts.map +1 -1
  26. package/dist/core/markdown.js +13 -3
  27. package/dist/core/templates.d.ts +12 -1
  28. package/dist/core/templates.d.ts.map +1 -1
  29. package/dist/core/templates.js +61 -6
  30. package/dist/core/utils/fs.utils.d.ts +21 -1
  31. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  32. package/dist/core/utils/fs.utils.js +61 -0
  33. package/dist/core/utils/index.d.ts +1 -1
  34. package/dist/core/utils/index.d.ts.map +1 -1
  35. package/dist/core/utils/index.js +1 -1
  36. package/dist/images/config.d.ts +52 -0
  37. package/dist/images/config.d.ts.map +1 -0
  38. package/dist/images/config.js +200 -0
  39. package/dist/images/index.d.ts +15 -0
  40. package/dist/images/index.d.ts.map +1 -0
  41. package/dist/images/index.js +11 -0
  42. package/dist/images/manifest.d.ts +36 -0
  43. package/dist/images/manifest.d.ts.map +1 -0
  44. package/dist/images/manifest.js +69 -0
  45. package/dist/images/markdown-rule.d.ts +26 -0
  46. package/dist/images/markdown-rule.d.ts.map +1 -0
  47. package/dist/images/markdown-rule.js +60 -0
  48. package/dist/images/markup.d.ts +39 -0
  49. package/dist/images/markup.d.ts.map +1 -0
  50. package/dist/images/markup.js +72 -0
  51. package/dist/images/og.d.ts +52 -0
  52. package/dist/images/og.d.ts.map +1 -0
  53. package/dist/images/og.js +165 -0
  54. package/dist/images/plan.d.ts +101 -0
  55. package/dist/images/plan.d.ts.map +1 -0
  56. package/dist/images/plan.js +128 -0
  57. package/dist/images/provider.d.ts +35 -0
  58. package/dist/images/provider.d.ts.map +1 -0
  59. package/dist/images/provider.js +70 -0
  60. package/dist/images/resolve.d.ts +45 -0
  61. package/dist/images/resolve.d.ts.map +1 -0
  62. package/dist/images/resolve.js +51 -0
  63. package/dist/images/runtime.d.ts +258 -0
  64. package/dist/images/runtime.d.ts.map +1 -0
  65. package/dist/images/runtime.js +659 -0
  66. package/dist/images/sources.d.ts +47 -0
  67. package/dist/images/sources.d.ts.map +1 -0
  68. package/dist/images/sources.js +97 -0
  69. package/dist/images/utils/image-probe.utils.d.ts +30 -0
  70. package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
  71. package/dist/images/utils/image-probe.utils.js +307 -0
  72. package/dist/images/utils/index.d.ts +7 -0
  73. package/dist/images/utils/index.d.ts.map +1 -0
  74. package/dist/images/utils/index.js +5 -0
  75. package/dist/index.d.ts +1 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/metrics/noop.d.ts.map +1 -1
  78. package/dist/metrics/noop.js +3 -0
  79. package/dist/metrics/recorder.d.ts.map +1 -1
  80. package/dist/metrics/recorder.js +3 -0
  81. package/dist/metrics/types.d.ts +8 -0
  82. package/dist/metrics/types.d.ts.map +1 -1
  83. package/dist/seo/auto-inject.d.ts +3 -0
  84. package/dist/seo/auto-inject.d.ts.map +1 -1
  85. package/dist/seo/auto-inject.js +1 -0
  86. package/dist/seo/generator.d.ts +24 -0
  87. package/dist/seo/generator.d.ts.map +1 -1
  88. package/dist/seo/generator.js +65 -1
  89. package/dist/seo/index.d.ts +1 -1
  90. package/dist/seo/index.d.ts.map +1 -1
  91. package/dist/seo/index.js +1 -1
  92. package/dist/types/config.d.ts +5 -0
  93. package/dist/types/config.d.ts.map +1 -1
  94. package/dist/types/content.d.ts +21 -2
  95. package/dist/types/content.d.ts.map +1 -1
  96. package/dist/types/images.d.ts +185 -0
  97. package/dist/types/images.d.ts.map +1 -0
  98. package/dist/types/images.js +8 -0
  99. package/dist/types/index.d.ts +1 -0
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/types/isg.d.ts +7 -0
  102. package/dist/types/isg.d.ts.map +1 -1
  103. package/dist/types/seo.d.ts +9 -0
  104. package/dist/types/seo.d.ts.map +1 -1
  105. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { join, dirname, relative, posix } from 'node:path';
2
2
  import { pathExists, readFile, isCollectionIndexPage, discoverLayout, resolveSrcDir, } from '../utils/index.js';
3
3
  import glob from 'fast-glob';
4
- import { TEMPLATE_EXTENSION } from '../../constants.js';
4
+ import { TEMPLATE_EXTENSION, DEFAULT_OG_TEMPLATE } from '../../constants.js';
5
+ import { clearOgTemplateCache, findOgTemplatePath, isOgImageExcluded } from '../../images/og.js';
5
6
  /**
6
7
  * Per-build cache for template path resolution (glob results).
7
8
  * Cleared at the start of each build via clearTemplatePathCache().
@@ -19,6 +20,7 @@ const templateContentCache = new Map();
19
20
  export function clearTemplatePathCache() {
20
21
  templatePathCache.clear();
21
22
  templateContentCache.clear();
23
+ clearOgTemplateCache();
22
24
  }
23
25
  /**
24
26
  * Error thrown when a circular dependency is detected in templates.
@@ -78,6 +80,13 @@ export async function trackTemplateDependencies(page, config, allPages) {
78
80
  // This populates 'visited' with all actually-used templates (not just accessible ones)
79
81
  await collectTemplateDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
80
82
  }
83
+ // The Open Graph template renders with the page, so it and its partials are dependencies too.
84
+ // It comes after the layout, which must stay first.
85
+ const ogTemplatePath = await resolveOgTemplateDependency(page, config);
86
+ if (ogTemplatePath) {
87
+ deps.push(ogTemplatePath);
88
+ await collectTemplateDependencies(ogTemplatePath, srcDir, visited, currentPath);
89
+ }
81
90
  // 2. Add all actually-used templates from the visited set
82
91
  // Filter to only include underscore directories (partials/components)
83
92
  // and normalize paths to POSIX format
@@ -110,6 +119,26 @@ export async function resolveLayoutDependency(page, config, allPages) {
110
119
  // Normalize to POSIX format for consistent manifest output across platforms
111
120
  return layoutPath ? posix.join(srcDir.replace(/\\/g, '/'), layoutPath) : null;
112
121
  }
122
+ /**
123
+ * Resolves the Open Graph template a page renders with, when Open Graph images are on and the
124
+ * page does not opt out. Pages that use the built-in template have no file dependency.
125
+ *
126
+ * @param page - The page model
127
+ * @param config - Stati configuration
128
+ * @returns Absolute path to the template (POSIX format), or null
129
+ */
130
+ export async function resolveOgTemplateDependency(page, config) {
131
+ const og = config.images ? config.images.og : undefined;
132
+ if (!og || !config.srcDir) {
133
+ return null;
134
+ }
135
+ const options = og === true ? {} : og;
136
+ if (isOgImageExcluded(page, options.exclude ?? [])) {
137
+ return null;
138
+ }
139
+ const templatePath = await findOgTemplatePath(page.sourcePath, resolveSrcDir(config), options.template ?? DEFAULT_OG_TEMPLATE);
140
+ return templatePath ? templatePath.replace(/\\/g, '/') : null;
141
+ }
113
142
  /**
114
143
  * Resolves a template name to its file path.
115
144
  * Used for explicit layout specifications in front matter.
@@ -33,6 +33,16 @@ export declare function computeContentHash(content: string, frontMatter: Record<
33
33
  * ```
34
34
  */
35
35
  export declare function computeFileHash(filePath: string): Promise<string | null>;
36
+ /**
37
+ * Computes a hash of a binary file, such as an image a page displays.
38
+ * Unlike {@link computeFileHash}, it hashes raw bytes, and a missing file hashes to a
39
+ * fixed sentinel: a page that references a missing image stays cached until the file
40
+ * appears, and then rebuilds.
41
+ *
42
+ * @param filePath - Absolute path to the file
43
+ * @returns `sha256-…` for an existing file, `missing:<path>` otherwise
44
+ */
45
+ export declare function computeBinaryFileHash(filePath: string): Promise<string>;
36
46
  /**
37
47
  * Computes a combined hash from content hash and dependency hashes.
38
48
  * This represents the complete input state for a page.
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AA+BA;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiBhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8B9E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACxB,MAAM,CASR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CAqCnE"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAgCA;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiBhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8B9E;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACxB,MAAM,CASR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CAqCnE"}
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFile, pathExists } from '../utils/index.js';
3
+ import { inspectSource } from '../../images/sources.js';
3
4
  /**
4
5
  * Creates a SHA-256 hash instance, updates it with data, and returns the hex digest.
5
6
  * Internal utility to eliminate duplicate hash creation patterns.
@@ -104,6 +105,19 @@ export async function computeFileHash(filePath) {
104
105
  return null;
105
106
  }
106
107
  }
108
+ /**
109
+ * Computes a hash of a binary file, such as an image a page displays.
110
+ * Unlike {@link computeFileHash}, it hashes raw bytes, and a missing file hashes to a
111
+ * fixed sentinel: a page that references a missing image stays cached until the file
112
+ * appears, and then rebuilds.
113
+ *
114
+ * @param filePath - Absolute path to the file
115
+ * @returns `sha256-…` for an existing file, `missing:<path>` otherwise
116
+ */
117
+ export async function computeBinaryFileHash(filePath) {
118
+ const info = await inspectSource(filePath);
119
+ return info ? `sha256-${info.hash}` : `missing:${filePath.replace(/\\/g, '/')}`;
120
+ }
107
121
  /**
108
122
  * Computes a combined hash from content hash and dependency hashes.
109
123
  * This represents the complete input state for a page.
@@ -8,10 +8,11 @@
8
8
  */
9
9
  export { loadCacheManifest, saveCacheManifest, createEmptyManifest } from './manifest.js';
10
10
  export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder.js';
11
+ export type { RenderedAssets } from './builder.js';
11
12
  export { BuildLockManager, withBuildLock } from './build-lock.js';
12
13
  export { DevServerLockManager } from './dev-server-lock.js';
13
14
  export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath, clearTemplatePathCache, } from './deps.js';
14
- export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
15
+ export { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
15
16
  export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
16
17
  export { ISGConfigurationError, validateISGConfig, validatePageISGOverrides, extractNumericOverride, } from './validation.js';
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
@@ -16,7 +16,7 @@ export { DevServerLockManager } from './dev-server-lock.js';
16
16
  // Dependency tracking
17
17
  export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath, clearTemplatePathCache, } from './deps.js';
18
18
  // Hash computation
19
- export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
19
+ export { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
20
20
  // TTL and aging
21
21
  export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
22
22
  // Validation
@@ -229,6 +229,14 @@ function validateCacheEntry(entry, path) {
229
229
  console.warn(`Invalid cache entry for ${path}: "maxAgeCapDays" must be a number if present`);
230
230
  return false;
231
231
  }
232
+ for (const field of ['assetDeps', 'images']) {
233
+ const value = entryObj[field];
234
+ if (value !== undefined &&
235
+ (!Array.isArray(value) || !value.every((item) => typeof item === 'string'))) {
236
+ console.warn(`Invalid cache entry for ${path}: "${field}" must be an array of strings`);
237
+ return false;
238
+ }
239
+ }
232
240
  // Validate that deps and tags arrays contain strings
233
241
  if (!entryObj.deps.every((dep) => typeof dep === 'string')) {
234
242
  console.warn(`Invalid cache entry for ${path}: all "deps" must be strings`);
@@ -1,5 +1,6 @@
1
1
  import MarkdownIt from 'markdown-it';
2
2
  import type { StatiConfig, TocEntry } from '../types/index.js';
3
+ import type { PageImages } from '../images/runtime.js';
3
4
  /**
4
5
  * Result of rendering markdown content.
5
6
  */
@@ -29,13 +30,21 @@ export declare function createMarkdownProcessor(config: StatiConfig): Promise<Ma
29
30
  * @returns Array of TOC entries extracted from headings (levels 2-6)
30
31
  */
31
32
  export declare function extractToc(content: string, md: MarkdownIt): TocEntry[];
33
+ /**
34
+ * Options for {@link renderMarkdown}.
35
+ */
36
+ export interface RenderMarkdownOptions {
37
+ /** Image state of the page being rendered; without it images are left as authored */
38
+ images?: PageImages | undefined;
39
+ }
32
40
  /**
33
41
  * Renders markdown content to HTML with optional TOC extraction.
34
42
  *
35
43
  * @param content - The markdown content to render
36
44
  * @param md - The configured MarkdownIt instance
37
45
  * @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
46
+ * @param options - Page-specific rendering state
38
47
  * @returns Object containing rendered HTML and TOC entries
39
48
  */
40
- export declare function renderMarkdown(content: string, md: MarkdownIt, tocEnabled?: boolean): MarkdownResult;
49
+ export declare function renderMarkdown(content: string, md: MarkdownIt, tocEnabled?: boolean, options?: RenderMarkdownOptions): MarkdownResult;
41
50
  //# sourceMappingURL=markdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI/D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,GAAG,EAAE,QAAQ,EAAE,CAAC;CACjB;AAQD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAmBD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAkDtF;AA6ED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,QAAQ,EAAE,CAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,UAAU,EACd,UAAU,GAAE,OAAc,GACzB,cAAc,CAWhB"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,GAAG,EAAE,QAAQ,EAAE,CAAC;CACjB;AAQD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAmBD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAsDtF;AA6ED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,QAAQ,EAAE,CAGtE;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,UAAU,EACd,UAAU,GAAE,OAAc,EAC1B,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAkBhB"}
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import path from 'node:path';
5
5
  import { slugify } from './utils/index.js';
6
6
  import { isDevelopment } from '../env.js';
7
+ import { installImageRule, selectEagerImage, IMAGES_ENV_KEY } from '../images/markdown-rule.js';
7
8
  /**
8
9
  * Cached markdown processor for dev mode to avoid re-creating on each rebuild.
9
10
  * This significantly improves rebuild performance by avoiding repeated plugin loading.
@@ -48,6 +49,9 @@ export async function createMarkdownProcessor(config) {
48
49
  linkify: true,
49
50
  typographer: true,
50
51
  });
52
+ // Image dimensions, lazy loading and responsive variants. Installed first so plugins and
53
+ // markdown.configure can still replace the image rule.
54
+ installImageRule(md);
51
55
  // Apply plugins from array format
52
56
  if (config.markdown?.plugins) {
53
57
  for (const plugin of config.markdown.plugins) {
@@ -168,14 +172,20 @@ export function extractToc(content, md) {
168
172
  * @param content - The markdown content to render
169
173
  * @param md - The configured MarkdownIt instance
170
174
  * @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
175
+ * @param options - Page-specific rendering state
171
176
  * @returns Object containing rendered HTML and TOC entries
172
177
  */
173
- export function renderMarkdown(content, md, tocEnabled = true) {
178
+ export function renderMarkdown(content, md, tocEnabled = true, options = {}) {
179
+ // The same env object goes to parsing and rendering so renderer rules see the page's state
180
+ const env = options.images ? { [IMAGES_ENV_KEY]: options.images } : {};
174
181
  // Parse content into tokens
175
- const tokens = md.parse(content, {});
182
+ const tokens = md.parse(content, env);
176
183
  // Extract TOC and inject anchor IDs
177
184
  const toc = extractAndInjectAnchors(tokens, tocEnabled);
185
+ if (options.images) {
186
+ options.images.eagerToken = selectEagerImage(tokens);
187
+ }
178
188
  // Render tokens to HTML
179
- const html = md.renderer.render(tokens, md.options, {});
189
+ const html = md.renderer.render(tokens, md.options, env);
180
190
  return { html, toc };
181
191
  }
@@ -1,5 +1,7 @@
1
1
  import { Eta } from 'eta';
2
2
  import type { StatiConfig, PageModel, NavNode, TocEntry, Logger } from '../types/index.js';
3
+ import type { PageImages } from '../images/runtime.js';
4
+ import type { GeneratedOgImage } from '../types/index.js';
3
5
  /**
4
6
  * Result of rendering a page template.
5
7
  */
@@ -8,6 +10,15 @@ export interface RenderResult {
8
10
  html: string;
9
11
  /** Number of templates loaded (layout + partials) */
10
12
  templatesLoaded: number;
13
+ /** Open Graph image generated for the page, if any */
14
+ ogImage?: GeneratedOgImage | undefined;
15
+ }
16
+ /**
17
+ * Page-specific state for {@link renderPage}.
18
+ */
19
+ export interface RenderPageExtras {
20
+ /** Image state of the page; without it `stati.image()` emits a plain `<img>` */
21
+ images?: PageImages | undefined;
11
22
  }
12
23
  /**
13
24
  * Clear the cached template engine. Call this when templates change in dev mode
@@ -15,5 +26,5 @@ export interface RenderResult {
15
26
  */
16
27
  export declare function clearTemplateEngineCache(): void;
17
28
  export declare function createTemplateEngine(config: StatiConfig): Eta;
18
- export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets, toc?: TocEntry[], logger?: Logger): Promise<RenderResult>;
29
+ export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets, toc?: TocEntry[], logger?: Logger, extras?: RenderPageExtras): Promise<RenderResult>;
19
30
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAmB3B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;CACzB;AAgND;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,CAAC,CA0MvB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAkB3B,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAgOD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA0PvB"}
@@ -5,7 +5,22 @@ import { createFallbackLogger } from './utils/index.js';
5
5
  import { TEMPLATE_EXTENSION } from '../constants.js';
6
6
  import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
7
7
  import { isDevelopment, isProduction } from '../env.js';
8
- import { generateSEO } from '../seo/index.js';
8
+ import { generateSEO, resolveGeneratedImagePlaceholders } from '../seo/index.js';
9
+ /**
10
+ * `stati.image()` when images are turned off: a plain `<img>`, attributes escaped.
11
+ */
12
+ function renderPlainImage(src, options = {}) {
13
+ if (typeof src !== 'string' || !src.trim()) {
14
+ return '';
15
+ }
16
+ const attributes = [
17
+ `src="${escapeHtml(src)}"`,
18
+ `alt="${escapeHtml(options.alt ?? '')}"`,
19
+ ...(options.title ? [`title="${escapeHtml(options.title)}"`] : []),
20
+ ...(options.class ? [`class="${escapeHtml(options.class)}"`] : []),
21
+ ];
22
+ return `<img ${attributes.join(' ')}>`;
23
+ }
9
24
  /**
10
25
  * Groups pages by their tags for aggregation purposes.
11
26
  *
@@ -215,8 +230,15 @@ export function createTemplateEngine(config) {
215
230
  }
216
231
  return eta;
217
232
  }
218
- export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger) {
233
+ export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger, extras) {
219
234
  const log = logger || createFallbackLogger();
235
+ const pageImages = extras?.images;
236
+ // The generated Open Graph image is known once the og template has rendered, after the
237
+ // partials. Until then stati.generateSEO() emits placeholders for its tags.
238
+ const ogState = {
239
+ image: undefined,
240
+ pending: pageImages?.wantsOg() ?? false,
241
+ };
220
242
  // Discover partials for this page's directory hierarchy
221
243
  const srcDir = resolveSrcDir(config);
222
244
  const relativePath = relative(srcDir, page.sourcePath);
@@ -274,12 +296,24 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
274
296
  version: getStatiVersion(),
275
297
  },
276
298
  // Stati utilities object with helper functions
277
- generateSEO: (tags) => generateSEO({ page, config, site: config.site }, tags),
299
+ generateSEO: (tags) => generateSEO({
300
+ page,
301
+ config,
302
+ site: config.site,
303
+ generatedImage: ogState.image,
304
+ deferGeneratedImage: ogState.pending,
305
+ }, tags),
278
306
  propValue,
307
+ // Image with dimensions, lazy loading and, with images.responsive, srcset variants
308
+ image: (src, options) => pageImages ? pageImages.renderHelperImage(src, options) : renderPlainImage(src, options),
279
309
  assets, // TypeScript bundle assets (available when typescript.enabled is true)
280
310
  // Set by callable partials invoked with props; always present so that
281
311
  // stati.props?.x works under the dev-mode context validation
282
312
  props: undefined,
313
+ // Set while the Open Graph template renders: { width, height }
314
+ og: undefined,
315
+ // The page's generated Open Graph image, once rendered: { url, path, width, height, alt }
316
+ ogImage: undefined,
283
317
  };
284
318
  // Render partials and store their content
285
319
  // Use multiple passes to allow partials to reference other partials
@@ -345,6 +379,23 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
345
379
  break;
346
380
  }
347
381
  }
382
+ // Render the Open Graph template with the layout's context, minus the page body, so the
383
+ // generated image is known before the layout renders its SEO tags
384
+ if (pageImages && ogState.pending) {
385
+ const ogBaseContext = {
386
+ ...baseContext,
387
+ content: '',
388
+ page: { ...baseContext.page, content: '' },
389
+ og: pageImages.ogSize(),
390
+ };
391
+ const ogContext = createValidatingContextProxy({
392
+ ...ogBaseContext,
393
+ partials: createValidatingPartialsProxy(wrapPartialsAsCallable(eta, renderedPartials, partialPaths, ogBaseContext)),
394
+ });
395
+ ogState.image = (await pageImages.renderOg(ogContext, eta)) ?? undefined;
396
+ ogState.pending = false;
397
+ baseContext.ogImage = ogState.image;
398
+ }
348
399
  // Wrap final rendered partials as callable before passing to layout context
349
400
  const callablePartials = wrapPartialsAsCallable(eta, renderedPartials, partialPaths, baseContext);
350
401
  const context = createValidatingContextProxy({
@@ -354,10 +405,14 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
354
405
  try {
355
406
  if (!layoutPath) {
356
407
  log.warning('No layout template found, using fallback');
357
- return { html: createFallbackHtml(page, body), templatesLoaded };
408
+ return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
358
409
  }
359
410
  const html = await eta.renderAsync(layoutPath, context);
360
- return { html, templatesLoaded };
411
+ return {
412
+ html: resolveGeneratedImagePlaceholders(html, ogState.image),
413
+ templatesLoaded,
414
+ ogImage: ogState.image,
415
+ };
361
416
  }
362
417
  catch (error) {
363
418
  log.error(`Error rendering layout ${layoutPath || 'unknown'}: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,7 +421,7 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
366
421
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
367
422
  throw templateError;
368
423
  }
369
- return { html: createFallbackHtml(page, body), templatesLoaded };
424
+ return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
370
425
  }
371
426
  }
372
427
  function createFallbackHtml(page, body) {
@@ -1,5 +1,5 @@
1
1
  import fse from 'fs-extra';
2
- import type { WriteFileOptions } from 'node:fs';
2
+ import type { WriteFileOptions, Stats } from 'node:fs';
3
3
  /**
4
4
  * Safely reads a file with consistent error handling.
5
5
  */
@@ -34,4 +34,24 @@ export declare function readdir<T extends boolean = false>(dirPath: string, opti
34
34
  * Copies a file with consistent error handling.
35
35
  */
36
36
  export declare function copyFile(src: string, dest: string): Promise<void>;
37
+ /**
38
+ * Reads a file as raw bytes. Returns null if the file does not exist.
39
+ */
40
+ export declare function readFileBuffer(filePath: string): Promise<Buffer | null>;
41
+ /**
42
+ * Reads a file as raw bytes synchronously. Returns null if the file does not exist.
43
+ * For code that must stay synchronous, such as template helpers.
44
+ */
45
+ export declare function readFileBufferSync(filePath: string): Buffer | null;
46
+ /**
47
+ * Gets file stats synchronously. Returns null if the path does not exist.
48
+ */
49
+ export declare function statSyncOrNull(filePath: string): Stats | null;
50
+ /**
51
+ * Lists every file under a directory, recursively.
52
+ *
53
+ * @param dirPath - Directory to list
54
+ * @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
55
+ */
56
+ export declare function listFilesRecursiveSync(dirPath: string): string[];
37
57
  //# sourceMappingURL=fs.utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAkDhD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,CAAC,CAAA;CAAE,GAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAcnD;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE"}
1
+ {"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAmDvD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,CAAC,CAAA;CAAE,GAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAcnD;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAU7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAchE"}
@@ -1,4 +1,6 @@
1
1
  import fse from 'fs-extra';
2
+ import { readFileSync, statSync, readdirSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
2
4
  const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
3
5
  /**
4
6
  * Wraps fs-extra operations with consistent error handling.
@@ -86,3 +88,62 @@ export async function readdir(dirPath, options) {
86
88
  export async function copyFile(src, dest) {
87
89
  return wrapFsOperation('copy', fseCopyFile(src, dest), `${src} to ${dest}`);
88
90
  }
91
+ /**
92
+ * Reads a file as raw bytes. Returns null if the file does not exist.
93
+ */
94
+ export async function readFileBuffer(filePath) {
95
+ return wrapFsOperationNullable('read file', fseReadFile(filePath), filePath);
96
+ }
97
+ /**
98
+ * Reads a file as raw bytes synchronously. Returns null if the file does not exist.
99
+ * For code that must stay synchronous, such as template helpers.
100
+ */
101
+ export function readFileBufferSync(filePath) {
102
+ try {
103
+ return readFileSync(filePath);
104
+ }
105
+ catch (error) {
106
+ const nodeError = error;
107
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
108
+ return null;
109
+ }
110
+ throw new Error(`Failed to read file ${filePath}: ${nodeError.message}`, { cause: error });
111
+ }
112
+ }
113
+ /**
114
+ * Gets file stats synchronously. Returns null if the path does not exist.
115
+ */
116
+ export function statSyncOrNull(filePath) {
117
+ try {
118
+ return statSync(filePath);
119
+ }
120
+ catch (error) {
121
+ const nodeError = error;
122
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
123
+ return null;
124
+ }
125
+ throw new Error(`Failed to get stats for ${filePath}: ${nodeError.message}`, { cause: error });
126
+ }
127
+ }
128
+ /**
129
+ * Lists every file under a directory, recursively.
130
+ *
131
+ * @param dirPath - Directory to list
132
+ * @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
133
+ */
134
+ export function listFilesRecursiveSync(dirPath) {
135
+ let entries;
136
+ try {
137
+ entries = readdirSync(dirPath, { withFileTypes: true, recursive: true });
138
+ }
139
+ catch (error) {
140
+ const nodeError = error;
141
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
142
+ return [];
143
+ }
144
+ throw new Error(`Failed to read directory ${dirPath}: ${nodeError.message}`, { cause: error });
145
+ }
146
+ return entries
147
+ .filter((entry) => entry.isFile())
148
+ .map((entry) => relative(dirPath, join(entry.parentPath, entry.name)).replace(/\\/g, '/'));
149
+ }
@@ -2,7 +2,7 @@
2
2
  * Core utilities index
3
3
  * @module core/utils
4
4
  */
5
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
5
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
6
  export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
7
7
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * @module core/utils
4
4
  */
5
5
  // File system utilities
6
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
6
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
7
7
  // Path resolution utilities
8
8
  export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
9
9
  // Template discovery utilities
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Validation and defaults for the `images` configuration block.
3
+ * @module images/config
4
+ */
5
+ import type { ImageVariantFormat, OgImageFontConfig, StatiConfig } from '../types/index.js';
6
+ /** Responsive image options with defaults applied. */
7
+ export interface ResolvedResponsiveConfig {
8
+ /** Candidate widths, ascending and unique */
9
+ widths: number[];
10
+ formats: ImageVariantFormat[];
11
+ quality: {
12
+ avif: number;
13
+ webp: number;
14
+ jpeg: number;
15
+ png?: number;
16
+ };
17
+ sizes: string;
18
+ }
19
+ /** Open Graph image options with defaults applied. */
20
+ export interface ResolvedOgConfig {
21
+ template: string;
22
+ width: number;
23
+ height: number;
24
+ format: 'png' | 'jpeg';
25
+ fonts: OgImageFontConfig[];
26
+ exclude: string[];
27
+ }
28
+ /** The `images` block with defaults applied. */
29
+ export interface ResolvedImagesConfig {
30
+ eagerFirst: boolean;
31
+ /** `null` when responsive images are off */
32
+ responsive: ResolvedResponsiveConfig | null;
33
+ /** `null` when Open Graph images are off */
34
+ og: ResolvedOgConfig | null;
35
+ dev: 'fast' | 'full';
36
+ }
37
+ /**
38
+ * Validates the `images` configuration block.
39
+ *
40
+ * @param images - The `images` value from the config
41
+ * @throws {ConfigValidationError} When an option has the wrong type or value
42
+ */
43
+ export declare function validateImagesConfig(images: unknown): void;
44
+ /**
45
+ * Applies defaults to the `images` configuration block.
46
+ * Expects a block that passed {@link validateImagesConfig}.
47
+ *
48
+ * @param images - The `images` value from the config
49
+ * @returns The resolved options, or `null` when images are turned off with `images: false`
50
+ */
51
+ export declare function resolveImagesConfig(images: StatiConfig['images']): ResolvedImagesConfig | null;
52
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/images/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAY5F,sDAAsD;AACtD,MAAM,WAAW,wBAAwB;IACvC,6CAA6C;IAC7C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,UAAU,EAAE,wBAAwB,GAAG,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,EAAE,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5B,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB;AA6JD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAqB1D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,oBAAoB,GAAG,IAAI,CA0C9F"}