@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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Error thrown when the Stati configuration is invalid.
3
+ * `loadConfig()` always rethrows it, because the build cannot proceed with a broken config.
4
+ */
5
+ export declare class ConfigValidationError extends Error {
6
+ constructor(message: string, options?: {
7
+ cause?: unknown;
8
+ });
9
+ }
10
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/config/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Error thrown when the Stati configuration is invalid.
3
+ * `loadConfig()` always rethrows it, because the build cannot proceed with a broken config.
4
+ */
5
+ export class ConfigValidationError extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = 'ConfigValidationError';
9
+ }
10
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA6BrD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8GlF"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA+BrD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8HlF"}
@@ -2,6 +2,8 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
5
+ import { validateImagesConfig } from '../images/config.js';
6
+ import { ConfigValidationError } from './errors.js';
5
7
  import { compileStatiConfig, cleanupCompiledConfig } from '../core/utils/index.js';
6
8
  import { DEFAULT_SRC_DIR, DEFAULT_OUT_DIR, DEFAULT_STATIC_DIR, DEFAULT_SITE_TITLE, DEFAULT_DEV_BASE_URL, DEFAULT_TTL_SECONDS, DEFAULT_MAX_AGE_CAP_DAYS, CONFIG_FILE_PATTERNS, } from '../constants.js';
7
9
  /**
@@ -139,6 +141,16 @@ export async function loadConfig(cwd = process.cwd()) {
139
141
  }
140
142
  throw error; // Re-throw non-ISG errors
141
143
  }
144
+ // Validate image configuration
145
+ try {
146
+ validateImagesConfig(mergedConfig.images);
147
+ }
148
+ catch (error) {
149
+ if (error instanceof ConfigValidationError) {
150
+ throw new ConfigValidationError(`Invalid images configuration in ${configPath}:\n${error.message}`, { cause: error });
151
+ }
152
+ throw error;
153
+ }
142
154
  // Cache the loaded config with its modification time
143
155
  try {
144
156
  const mtime = statSync(resolvedConfigPath).mtimeMs;
@@ -154,8 +166,9 @@ export async function loadConfig(cwd = process.cwd()) {
154
166
  return mergedConfig;
155
167
  }
156
168
  catch (error) {
157
- if (error instanceof Error && error.message.includes('Invalid ISG configuration')) {
158
- // ISG validation errors should bubble up with context
169
+ if (error instanceof ConfigValidationError ||
170
+ (error instanceof Error && error.message.includes('Invalid ISG configuration'))) {
171
+ // Validation errors bubble up with context: the build cannot proceed with an invalid config
159
172
  throw error;
160
173
  }
161
174
  console.error(`Error loading config from ${configPath}:`, error);
@@ -82,4 +82,30 @@ export declare const DEFAULT_BUNDLES: readonly [{
82
82
  readonly entryPoint: "main.ts";
83
83
  readonly bundleName: "main";
84
84
  }];
85
+ /** Output directory for generated and copied images, relative to the build output */
86
+ export declare const IMAGES_OUTPUT_DIR = "_img";
87
+ /** Subdirectory of {@link IMAGES_OUTPUT_DIR} for generated Open Graph images */
88
+ export declare const OG_IMAGES_SUBDIR = "og";
89
+ /** Cache subdirectory (inside `.stati`) holding the images manifest */
90
+ export declare const IMAGES_CACHE_SUBDIR = "images";
91
+ /** Package that implements responsive images and Open Graph image generation */
92
+ export declare const IMAGES_PACKAGE_NAME = "@stati/images";
93
+ /** Default candidate widths for responsive images */
94
+ export declare const DEFAULT_IMAGE_WIDTHS: readonly number[];
95
+ /** Default modern formats for responsive images */
96
+ export declare const DEFAULT_IMAGE_FORMATS: readonly ('avif' | 'webp')[];
97
+ /** Default encoder quality per format; PNG stays lossless unless configured */
98
+ export declare const DEFAULT_IMAGE_QUALITY: {
99
+ readonly avif: 50;
100
+ readonly webp: 75;
101
+ readonly jpeg: 80;
102
+ };
103
+ /** Default `sizes` attribute for responsive images */
104
+ export declare const DEFAULT_IMAGE_SIZES = "100vw";
105
+ /** Default Open Graph image template name */
106
+ export declare const DEFAULT_OG_TEMPLATE = "og.eta";
107
+ /** Default Open Graph image width in pixels */
108
+ export declare const DEFAULT_OG_WIDTH = 1200;
109
+ /** Default Open Graph image height in pixels */
110
+ export declare const DEFAULT_OG_HEIGHT = 630;
85
111
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,iDAAiD;AACjD,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC,+CAA+C;AAC/C,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC,sCAAsC;AACtC,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C,iCAAiC;AACjC,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,4DAA4D;AAC5D,eAAO,MAAM,YAAY,UAAU,CAAC;AAEpC,kCAAkC;AAClC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAIjD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,mCAAmC;AACnC,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,UAEhC,CAAC;AAIF,sCAAsC;AACtC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,kCAAkC;AAClC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,sCAAsC;AACtC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,0BAAmD,CAAC;AAIrF,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,qEAAqE;AACrE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAI9C,4BAA4B;AAC5B,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,0BAA0B;AAC1B,eAAO,MAAM,gBAAgB,QAA0B,CAAC;AAExD,yBAAyB;AACzB,eAAO,MAAM,eAAe,QAAwB,CAAC;AAErD,0BAA0B;AAC1B,eAAO,MAAM,gBAAgB,QAAsB,CAAC;AAEpD,8BAA8B;AAC9B,eAAO,MAAM,oBAAoB,QAAyB,CAAC;AAI3D,oDAAoD;AACpD,eAAO,MAAM,aAAa,QAAyB,CAAC;AAEpD,iDAAiD;AACjD,eAAO,MAAM,cAAc,QAA0B,CAAC;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,UAAU,QAAmB,CAAC;AAE3C,iDAAiD;AACjD,eAAO,MAAM,WAAW,QAAuB,CAAC;AAEhD,uCAAuC;AACvC,eAAO,MAAM,WAAW,QAAsB,CAAC;AAE/C,2CAA2C;AAC3C,eAAO,MAAM,SAAS,QAAkB,CAAC;AAEzC,6CAA6C;AAC7C,eAAO,MAAM,UAAU,QAAmB,CAAC;AAI3C,kCAAkC;AAClC,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,+BAA+B;AAC/B,eAAO,MAAM,eAAe,eAAgC,CAAC;AAE7D,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAIvC,yBAAyB;AACzB,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,8CAA8C;AAC9C,eAAO,MAAM,cAAc,UAAU,CAAC;AAItC,0CAA0C;AAC1C,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,qEAAqE;AACrE,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,eAAe;;;EAA2D,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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"}
package/dist/constants.js CHANGED
@@ -88,3 +88,26 @@ export const DEFAULT_TS_OUT_DIR = '_assets';
88
88
  * Creates a single global bundle from 'main.ts' entry point.
89
89
  */
90
90
  export const DEFAULT_BUNDLES = [{ entryPoint: 'main.ts', bundleName: 'main' }];
91
+ // === Image Constants ===
92
+ /** Output directory for generated and copied images, relative to the build output */
93
+ export const IMAGES_OUTPUT_DIR = '_img';
94
+ /** Subdirectory of {@link IMAGES_OUTPUT_DIR} for generated Open Graph images */
95
+ export const OG_IMAGES_SUBDIR = 'og';
96
+ /** Cache subdirectory (inside `.stati`) holding the images manifest */
97
+ export const IMAGES_CACHE_SUBDIR = 'images';
98
+ /** Package that implements responsive images and Open Graph image generation */
99
+ export const IMAGES_PACKAGE_NAME = '@stati/images';
100
+ /** Default candidate widths for responsive images */
101
+ export const DEFAULT_IMAGE_WIDTHS = [480, 960, 1440];
102
+ /** Default modern formats for responsive images */
103
+ export const DEFAULT_IMAGE_FORMATS = ['avif', 'webp'];
104
+ /** Default encoder quality per format; PNG stays lossless unless configured */
105
+ export const DEFAULT_IMAGE_QUALITY = { avif: 50, webp: 75, jpeg: 80 };
106
+ /** Default `sizes` attribute for responsive images */
107
+ export const DEFAULT_IMAGE_SIZES = '100vw';
108
+ /** Default Open Graph image template name */
109
+ export const DEFAULT_OG_TEMPLATE = 'og.eta';
110
+ /** Default Open Graph image width in pixels */
111
+ export const DEFAULT_OG_WIDTH = 1200;
112
+ /** Default Open Graph image height in pixels */
113
+ export const DEFAULT_OG_HEIGHT = 630;
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA2DA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA2DA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
@@ -12,6 +12,7 @@ import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
12
12
  import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, } from '../search/index.js';
13
13
  import { getEnv, isDevelopment } from '../env.js';
14
14
  import { DEFAULT_OUT_DIR } from '../constants.js';
15
+ import { ImageRuntime } from '../images/runtime.js';
15
16
  import { createMetricRecorder, noopMetricRecorder } from '../metrics/index.js';
16
17
  /**
17
18
  * Recursively calculates the total size of a directory in bytes.
@@ -203,10 +204,29 @@ async function loadContentAndBuildNavigation(config, options, logger) {
203
204
  const eta = createTemplateEngine(config);
204
205
  return { pages, navigation, md, eta, navigationHash };
205
206
  }
207
+ /**
208
+ * Output file and ISG cache key of a page.
209
+ */
210
+ function getPageOutput(page, outDir) {
211
+ let outputPath;
212
+ if (page.url === '/') {
213
+ outputPath = join(outDir, 'index.html');
214
+ }
215
+ else if (page.url.endsWith('/')) {
216
+ outputPath = join(outDir, page.url, 'index.html');
217
+ }
218
+ else {
219
+ outputPath = join(outDir, `${page.url}.html`);
220
+ }
221
+ // Cache key is the output path relative to outDir
222
+ const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
223
+ const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
224
+ return { outputPath, cacheKey };
225
+ }
206
226
  /**
207
227
  * Processes pages with ISG caching logic.
208
228
  */
209
- async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename) {
229
+ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename, imageRuntime = null) {
210
230
  let cacheHits = 0;
211
231
  let cacheMisses = 0;
212
232
  const searchablePages = [];
@@ -236,24 +256,14 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
236
256
  const isDevMode = isDevelopment();
237
257
  const pendingWrites = [];
238
258
  const pendingCacheUpdates = [];
259
+ // Image settings that change generated HTML, part of every page's render inputs
260
+ const imageFingerprint = imageRuntime?.fingerprint();
239
261
  for (let i = 0; i < pages.length; i++) {
240
262
  const page = pages[i];
241
263
  if (!page)
242
264
  continue; // Safety check
243
- // Determine output path
244
- let outputPath;
245
- if (page.url === '/') {
246
- outputPath = join(outDir, 'index.html');
247
- }
248
- else if (page.url.endsWith('/')) {
249
- outputPath = join(outDir, page.url, 'index.html');
250
- }
251
- else {
252
- outputPath = join(outDir, `${page.url}.html`);
253
- }
254
- // Get cache key (use output path relative to outDir)
255
- const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
256
- const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
265
+ // Determine output path and cache key
266
+ const { outputPath, cacheKey } = getPageOutput(page, outDir);
257
267
  const existingEntry = manifest.entries[cacheKey];
258
268
  const bundlePaths = getBundlePathsForPage(page.url, compiledBundles);
259
269
  const assets = {
@@ -267,12 +277,16 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
267
277
  ...assets,
268
278
  bundleAutoInject: config.typescript?.autoInject !== false,
269
279
  searchAutoInject: config.search?.autoInjectMetaTag !== false,
280
+ // Omitted with images: false, so the hash matches builds without the image pipeline
281
+ ...(imageFingerprint && { images: imageFingerprint }),
270
282
  });
271
283
  // Check if we should rebuild this page (considering ISG logic)
272
284
  const shouldRebuildStart = performance.now();
273
285
  const shouldRebuild = options.force ||
274
286
  config.isg?.enabled === false ||
275
287
  !(await pathExists(outputPath)) ||
288
+ // A cached page whose generated images are gone is rebuilt to regenerate them
289
+ (imageRuntime !== null && !imageRuntime.outputsExist(existingEntry?.images)) ||
276
290
  (await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
277
291
  const shouldRebuildTime = performance.now() - shouldRebuildStart;
278
292
  totalShouldRebuildTime += shouldRebuildTime;
@@ -302,12 +316,16 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
302
316
  await config.hooks.beforeRender({ page, config });
303
317
  recorder.addToPhase('hookBeforeRenderTotalMs', performance.now() - hookStart);
304
318
  }
319
+ // Image state for this page: markup, dependencies and generated files
320
+ const pageImages = imageRuntime?.createPageContext(page);
305
321
  // Render markdown to HTML with TOC extraction
306
322
  const tocEnabled = config.markdown?.toc !== false;
307
- const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled);
323
+ const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled, {
324
+ images: pageImages,
325
+ });
308
326
  // Render with template
309
327
  const renderPageStart = performance.now();
310
- const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger);
328
+ const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger, { images: pageImages });
311
329
  const renderPageTime = performance.now() - renderPageStart;
312
330
  totalRenderTime += renderPageTime;
313
331
  let finalHtml = renderResult.html;
@@ -324,6 +342,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
324
342
  if (config.seo?.debug !== undefined) {
325
343
  injectOptions.debug = config.seo.debug;
326
344
  }
345
+ if (renderResult.ogImage) {
346
+ injectOptions.generatedImage = renderResult.ogImage;
347
+ }
327
348
  finalHtml = autoInjectSEO(finalHtml, injectOptions);
328
349
  }
329
350
  // Auto-inject search index meta tag if enabled (default: true)
@@ -336,6 +357,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
336
357
  if (config.typescript?.autoInject !== false && assets.bundlePaths.length > 0) {
337
358
  finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
338
359
  }
360
+ // Start the image jobs the final HTML needs and record its image files for the cache
361
+ const renderedAssets = pageImages && imageRuntime
362
+ ? imageRuntime.commitPage(pageImages, finalHtml, { outputPath, cacheKey })
363
+ : undefined;
339
364
  const renderTime = Math.round(performance.now() - startTime);
340
365
  // Record page timing for rendered pages (includes template count)
341
366
  recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
@@ -345,7 +370,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
345
370
  // In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
346
371
  if (isDevMode) {
347
372
  pendingWrites.push({ outputPath, content: finalHtml });
348
- pendingCacheUpdates.push({ cacheKey, page, existingEntry, renderInputsHash });
373
+ pendingCacheUpdates.push({ cacheKey, page, existingEntry, renderInputsHash, renderedAssets });
349
374
  }
350
375
  else {
351
376
  // Production mode: write immediately
@@ -357,10 +382,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
357
382
  if (!options.skipCacheUpdate) {
358
383
  const cacheEntryStart = performance.now();
359
384
  if (existingEntry) {
360
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderInputsHash);
385
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderInputsHash, renderedAssets);
361
386
  }
362
387
  else {
363
- manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash);
388
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash, renderedAssets);
364
389
  }
365
390
  totalCacheEntryTime += performance.now() - cacheEntryStart;
366
391
  }
@@ -390,14 +415,14 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
390
415
  // This avoids expensive template dependency tracking on each incremental rebuild
391
416
  if (!options.skipCacheUpdate) {
392
417
  const cacheEntryStart = performance.now();
393
- for (const { cacheKey, page, existingEntry, renderInputsHash } of pendingCacheUpdates) {
418
+ for (const { cacheKey, page, existingEntry, renderInputsHash, renderedAssets, } of pendingCacheUpdates) {
394
419
  if (existingEntry) {
395
420
  // Fast update: reuse existing deps, just update hashes and timestamp
396
421
  manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, true, // fastUpdate = true in dev mode
397
- renderInputsHash);
422
+ renderInputsHash, renderedAssets);
398
423
  }
399
424
  else {
400
- manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash);
425
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash, renderedAssets);
401
426
  }
402
427
  }
403
428
  totalCacheEntryTime = performance.now() - cacheEntryStart;
@@ -501,6 +526,14 @@ async function buildInternal(options = {}) {
501
526
  await remove(cacheDir);
502
527
  }
503
528
  await ensureDir(outDir);
529
+ // Image pipeline; null with images: false. Fails here, before any page renders, when a
530
+ // feature needs @stati/images and it is missing or incompatible.
531
+ const imageRuntime = await ImageRuntime.create(config, {
532
+ outDir,
533
+ cacheDir,
534
+ logger,
535
+ development: isDevelopment(),
536
+ });
504
537
  // Enable Tailwind class inventory tracking only if Tailwind is detected
505
538
  const tailwindInitStart = performance.now();
506
539
  const hasTailwind = await isTailwindUsed();
@@ -593,9 +626,26 @@ async function buildInternal(options = {}) {
593
626
  }
594
627
  const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
595
628
  const buildTime = new Date();
596
- const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename);
629
+ const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename, imageRuntime);
597
630
  endPageRenderSpan();
598
631
  const { cacheHits, cacheMisses, searchablePages } = pageProcessingResult;
632
+ // Wait for image jobs; pages whose images failed are rewritten to show plain images
633
+ let imageFailures = [];
634
+ if (imageRuntime) {
635
+ const endImageSpan = recorder.startSpan('imageSettleMs');
636
+ const settled = await imageRuntime.settle(manifest);
637
+ await imageRuntime.save();
638
+ endImageSpan();
639
+ imageFailures = settled.failures;
640
+ const { generated, cached, failed } = imageRuntime.stats;
641
+ recorder.increment('imagesGenerated', generated);
642
+ recorder.increment('imagesCached', cached);
643
+ recorder.increment('imagesFailed', failed);
644
+ if (generated > 0 || failed > 0) {
645
+ logger.info('');
646
+ logger.info(`Images: ${generated} generated, ${cached} reused${failed > 0 ? `, ${failed} failed` : ''}`);
647
+ }
648
+ }
599
649
  // Generate search index if enabled
600
650
  // Uses markdown content and TOC entries
601
651
  let searchIndexMetadata;
@@ -638,6 +688,13 @@ async function buildInternal(options = {}) {
638
688
  await saveCacheManifest(cacheDir, manifest);
639
689
  }
640
690
  endManifestSaveSpan();
691
+ // In the dev server a failed Open Graph image goes to the error overlay, like a template error
692
+ const ogFailures = imageFailures.filter((failure) => failure.og);
693
+ if (isDevMode && ogFailures.length > 0) {
694
+ throw new Error(ogFailures
695
+ .map((failure) => `Open Graph image failed for ${failure.label}: ${failure.message}`)
696
+ .join('\n'));
697
+ }
641
698
  // Copy static assets and count them (skip for template-only changes in dev mode)
642
699
  const endAssetSpan = recorder.startSpan('assetCopyMs');
643
700
  let assetsCount = 0;
@@ -646,6 +703,21 @@ async function buildInternal(options = {}) {
646
703
  }
647
704
  endAssetSpan();
648
705
  recorder.increment('assetsCopied', assetsCount);
706
+ // Delete generated images no page of this build uses. Production only: dev builds skip
707
+ // responsive and Open Graph images by default and must not delete them.
708
+ if (imageRuntime && getEnv() === 'production') {
709
+ const liveImages = new Set();
710
+ const liveSources = new Set();
711
+ for (const page of pages) {
712
+ const entry = manifest.entries[getPageOutput(page, outDir).cacheKey];
713
+ entry?.images?.forEach((image) => liveImages.add(image));
714
+ entry?.assetDeps?.forEach((source) => liveSources.add(source));
715
+ }
716
+ const removed = await imageRuntime.prune(liveImages, liveSources);
717
+ if (removed > 0) {
718
+ logger.info(`Removed ${removed} unused generated image${removed === 1 ? '' : 's'}`);
719
+ }
720
+ }
649
721
  // Get current environment
650
722
  const currentEnv = getEnv();
651
723
  // Generate sitemap if enabled (only in production mode)
@@ -1,4 +1,14 @@
1
1
  import type { PageModel, CacheEntry, StatiConfig } from '../../types/index.js';
2
+ /**
3
+ * Files a render discovered: the source images a page displays (`assetDeps`) and the
4
+ * generated images it references (`images`).
5
+ */
6
+ export interface RenderedAssets {
7
+ /** Absolute paths of files whose content affects the page, such as source images */
8
+ assetDeps: readonly string[];
9
+ /** Generated image files the page references, relative to the output directory */
10
+ images: readonly string[];
11
+ }
2
12
  /**
3
13
  * Determines if a page should be rebuilt based on ISG logic.
4
14
  * Checks content changes, dependency changes, TTL expiration, and freeze status.
@@ -29,6 +39,7 @@ export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | u
29
39
  * @param allPages - All pages in the site, used to resolve the same layout as rendering
30
40
  * @param renderedAt - When the page was rendered
31
41
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
42
+ * @param rendered - Files the render discovered, such as images
32
43
  * @returns New cache entry
33
44
  *
34
45
  * @example
@@ -36,7 +47,7 @@ export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | u
36
47
  * const entry = await createCacheEntry(page, config, pages, new Date());
37
48
  * ```
38
49
  */
39
- export declare function createCacheEntry(page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, renderInputsHash?: string): Promise<CacheEntry>;
50
+ export declare function createCacheEntry(page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, renderInputsHash?: string, rendered?: RenderedAssets): Promise<CacheEntry>;
40
51
  /**
41
52
  * Updates an existing cache entry with new information after rebuilding.
42
53
  * In dev mode, this is optimized to reuse the existing deps array since
@@ -49,6 +60,7 @@ export declare function createCacheEntry(page: PageModel, config: StatiConfig, a
49
60
  * @param renderedAt - When the page was rendered
50
61
  * @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
51
62
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
63
+ * @param rendered - Files the render discovered, such as images
52
64
  * @returns Updated cache entry
53
65
  *
54
66
  * @example
@@ -56,5 +68,5 @@ export declare function createCacheEntry(page: PageModel, config: StatiConfig, a
56
68
  * const updatedEntry = await updateCacheEntry(existingEntry, page, config, pages, new Date());
57
69
  * ```
58
70
  */
59
- export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, fastUpdate?: boolean, renderInputsHash?: string): Promise<CacheEntry>;
71
+ export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, fastUpdate?: boolean, renderInputsHash?: string, rendered?: RenderedAssets): Promise<CacheEntry>;
60
72
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAqE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,GAAG,EAAE,IAAI,EACT,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,OAAO,CAAC,CAoKlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CAqFrB;AAkDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,UAAU,UAAQ,EAClB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CAmDrB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAgF/E;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,kFAAkF;IAClF,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AA0BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,GAAG,EAAE,IAAI,EACT,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,OAAO,CAAC,CAuKlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,UAAU,CAAC,CAsFrB;AAkDD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,UAAU,UAAQ,EAClB,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,UAAU,CAAC,CA+DrB"}
@@ -1,4 +1,4 @@
1
- import { computeContentHash, computeFileHash, computeInputsHash } from './hash.js';
1
+ import { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, } from './hash.js';
2
2
  import { trackTemplateDependencies, resolveLayoutDependency } from './deps.js';
3
3
  import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
4
4
  import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
@@ -55,8 +55,35 @@ function isValidCacheEntry(entry) {
55
55
  if (entry.maxAgeCapDays !== undefined && typeof entry.maxAgeCapDays !== 'number') {
56
56
  return false;
57
57
  }
58
+ for (const list of [entry.assetDeps, entry.images]) {
59
+ if (list !== undefined && (!Array.isArray(list) || !list.every((v) => typeof v === 'string'))) {
60
+ return false;
61
+ }
62
+ }
58
63
  return true;
59
64
  }
65
+ /**
66
+ * Hashes render-discovered dependencies by content. Missing files hash to a sentinel.
67
+ */
68
+ async function hashAssetDeps(assetDeps) {
69
+ const hashes = [];
70
+ for (const assetPath of assetDeps ?? []) {
71
+ hashes.push(await computeBinaryFileHash(assetPath));
72
+ }
73
+ return hashes;
74
+ }
75
+ /**
76
+ * Returns the entry with its `assetDeps` and `images` replaced by this render's,
77
+ * dropping the fields when a render found none.
78
+ */
79
+ function withRenderedAssets(entry, rendered) {
80
+ const { assetDeps: _assetDeps, images: _images, ...rest } = entry;
81
+ return {
82
+ ...rest,
83
+ ...(rendered && rendered.assetDeps.length > 0 && { assetDeps: [...rendered.assetDeps] }),
84
+ ...(rendered && rendered.images.length > 0 && { images: [...rendered.images] }),
85
+ };
86
+ }
60
87
  /**
61
88
  * Determines if a page should be rebuilt based on ISG logic.
62
89
  * Checks content changes, dependency changes, TTL expiration, and freeze status.
@@ -133,6 +160,8 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
133
160
  console.warn(`${dependencyErrors} dependency errors for ${page.url}, forcing rebuild`);
134
161
  return true;
135
162
  }
163
+ // Files found during the last render, such as images, are hashed by content
164
+ depsHashes.push(...(await hashAssetDeps(entry.assetDeps)));
136
165
  const currentInputsHash = computeInputsHash(currentContentHash, depsHashes, renderInputsHash);
137
166
  // If inputs changed, always rebuild
138
167
  if (currentInputsHash !== entry.inputsHash) {
@@ -215,6 +244,7 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
215
244
  * @param allPages - All pages in the site, used to resolve the same layout as rendering
216
245
  * @param renderedAt - When the page was rendered
217
246
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
247
+ * @param rendered - Files the render discovered, such as images
218
248
  * @returns New cache entry
219
249
  *
220
250
  * @example
@@ -222,7 +252,7 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
222
252
  * const entry = await createCacheEntry(page, config, pages, new Date());
223
253
  * ```
224
254
  */
225
- export async function createCacheEntry(page, config, allPages, renderedAt, renderInputsHash) {
255
+ export async function createCacheEntry(page, config, allPages, renderedAt, renderInputsHash, rendered) {
226
256
  const timings = {};
227
257
  let start = performance.now();
228
258
  // Validate page-level ISG overrides first
@@ -245,6 +275,7 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
245
275
  depsHashes.push(depHash);
246
276
  }
247
277
  }
278
+ depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
248
279
  timings.depsHashes = performance.now() - start;
249
280
  const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
250
281
  // Extract tags from front matter
@@ -287,7 +318,7 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
287
318
  if (maxAgeCapDays !== undefined) {
288
319
  cacheEntry.maxAgeCapDays = maxAgeCapDays;
289
320
  }
290
- return cacheEntry;
321
+ return withRenderedAssets(cacheEntry, rendered);
291
322
  }
292
323
  /**
293
324
  * Checks if the template structure has changed by comparing the layout file's
@@ -345,6 +376,7 @@ async function hasTemplateStructureChanged(entry) {
345
376
  * @param renderedAt - When the page was rendered
346
377
  * @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
347
378
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
379
+ * @param rendered - Files the render discovered, such as images
348
380
  * @returns Updated cache entry
349
381
  *
350
382
  * @example
@@ -352,7 +384,7 @@ async function hasTemplateStructureChanged(entry) {
352
384
  * const updatedEntry = await updateCacheEntry(existingEntry, page, config, pages, new Date());
353
385
  * ```
354
386
  */
355
- export async function updateCacheEntry(entry, page, config, allPages, renderedAt, fastUpdate = false, renderInputsHash) {
387
+ export async function updateCacheEntry(entry, page, config, allPages, renderedAt, fastUpdate = false, renderInputsHash, rendered) {
356
388
  // In fast update mode (dev), reuse existing deps to avoid expensive tracking
357
389
  if (fastUpdate && entry.deps.length > 0) {
358
390
  // Detect if template structure has changed (new includes/extends added)
@@ -380,16 +412,18 @@ export async function updateCacheEntry(entry, page, config, allPages, renderedAt
380
412
  depsHashes.push(depHash);
381
413
  }
382
414
  }
415
+ // Asset deps come from this render, not from the previous entry
416
+ depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
383
417
  const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
384
- return {
418
+ return withRenderedAssets({
385
419
  ...entry,
386
420
  inputsHash,
387
421
  renderedAt: renderedAt.toISOString(),
388
- };
422
+ }, rendered);
389
423
  }
390
424
  }
391
425
  // Full update - create new entry and preserve original publishedAt
392
- const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash);
426
+ const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash, rendered);
393
427
  // Preserve original publishedAt if no new one is specified
394
428
  if (!newEntry.publishedAt && entry.publishedAt) {
395
429
  newEntry.publishedAt = entry.publishedAt;
@@ -50,6 +50,15 @@ export declare function trackTemplateDependencies(page: PageModel, config: Stati
50
50
  * @returns Absolute path to the layout file (POSIX format), or null if no layout applies
51
51
  */
52
52
  export declare function resolveLayoutDependency(page: PageModel, config: StatiConfig, allPages: PageModel[]): Promise<string | null>;
53
+ /**
54
+ * Resolves the Open Graph template a page renders with, when Open Graph images are on and the
55
+ * page does not opt out. Pages that use the built-in template have no file dependency.
56
+ *
57
+ * @param page - The page model
58
+ * @param config - Stati configuration
59
+ * @returns Absolute path to the template (POSIX format), or null
60
+ */
61
+ export declare function resolveOgTemplateDependency(page: PageModel, config: StatiConfig): Promise<string | null>;
53
62
  /**
54
63
  * Resolves a template name to its file path.
55
64
  * Used for explicit layout specifications in front matter.
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAenE;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CAuCnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAexB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAgBnE;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAexB;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAexB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}