@stati/core 1.29.2 → 2.0.1

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 (104) hide show
  1. package/README.md +10 -13
  2. package/dist/config/loader.d.ts +9 -0
  3. package/dist/config/loader.d.ts.map +1 -1
  4. package/dist/config/loader.js +77 -1
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +39 -29
  7. package/dist/core/content.d.ts.map +1 -1
  8. package/dist/core/content.js +9 -7
  9. package/dist/core/dev.d.ts.map +1 -1
  10. package/dist/core/dev.js +156 -197
  11. package/dist/core/eta-plugin.d.ts +60 -0
  12. package/dist/core/eta-plugin.d.ts.map +1 -0
  13. package/dist/core/eta-plugin.js +143 -0
  14. package/dist/core/isg/builder.d.ts +37 -19
  15. package/dist/core/isg/builder.d.ts.map +1 -1
  16. package/dist/core/isg/builder.js +70 -153
  17. package/dist/core/isg/deps.d.ts +8 -70
  18. package/dist/core/isg/deps.d.ts.map +1 -1
  19. package/dist/core/isg/deps.js +10 -401
  20. package/dist/core/isg/hash.d.ts.map +1 -1
  21. package/dist/core/isg/hash.js +2 -1
  22. package/dist/core/isg/index.d.ts +2 -2
  23. package/dist/core/isg/index.d.ts.map +1 -1
  24. package/dist/core/isg/index.js +3 -3
  25. package/dist/core/isg/manifest.d.ts.map +1 -1
  26. package/dist/core/isg/manifest.js +6 -1
  27. package/dist/core/markdown.d.ts +1 -1
  28. package/dist/core/markdown.d.ts.map +1 -1
  29. package/dist/core/markdown.js +3 -2
  30. package/dist/core/template-index.d.ts +209 -0
  31. package/dist/core/template-index.d.ts.map +1 -0
  32. package/dist/core/template-index.js +484 -0
  33. package/dist/core/template-render.d.ts +137 -0
  34. package/dist/core/template-render.d.ts.map +1 -0
  35. package/dist/core/template-render.js +444 -0
  36. package/dist/core/templates.d.ts +29 -11
  37. package/dist/core/templates.d.ts.map +1 -1
  38. package/dist/core/templates.js +115 -196
  39. package/dist/core/utils/context-validation.utils.d.ts +10 -6
  40. package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
  41. package/dist/core/utils/context-validation.utils.js +39 -16
  42. package/dist/core/utils/date.utils.d.ts +1 -1
  43. package/dist/core/utils/date.utils.js +1 -1
  44. package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
  45. package/dist/core/utils/dev-css-watcher.utils.js +3 -2
  46. package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
  47. package/dist/core/utils/error-overlay.utils.js +7 -45
  48. package/dist/core/utils/front-matter.utils.d.ts +50 -0
  49. package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
  50. package/dist/core/utils/front-matter.utils.js +157 -0
  51. package/dist/core/utils/index.d.ts +4 -5
  52. package/dist/core/utils/index.d.ts.map +1 -1
  53. package/dist/core/utils/index.js +5 -7
  54. package/dist/core/utils/paths.utils.d.ts +3 -3
  55. package/dist/core/utils/paths.utils.js +6 -6
  56. package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
  57. package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
  58. package/dist/core/utils/tailwind-inventory.utils.js +15 -6
  59. package/dist/core/utils/template-discovery.utils.d.ts +3 -14
  60. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  61. package/dist/core/utils/template-discovery.utils.js +2 -62
  62. package/dist/core/utils/template-errors.utils.d.ts +14 -1
  63. package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
  64. package/dist/core/utils/template-errors.utils.js +45 -4
  65. package/dist/core/utils/theme.utils.d.ts +17 -0
  66. package/dist/core/utils/theme.utils.d.ts.map +1 -0
  67. package/dist/core/utils/theme.utils.js +59 -0
  68. package/dist/images/index.d.ts +1 -2
  69. package/dist/images/index.d.ts.map +1 -1
  70. package/dist/images/index.js +0 -1
  71. package/dist/images/markdown-rule.d.ts +1 -2
  72. package/dist/images/markdown-rule.d.ts.map +1 -1
  73. package/dist/images/og.d.ts +2 -15
  74. package/dist/images/og.d.ts.map +1 -1
  75. package/dist/images/og.js +4 -47
  76. package/dist/images/runtime.d.ts +24 -5
  77. package/dist/images/runtime.d.ts.map +1 -1
  78. package/dist/images/runtime.js +14 -12
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/metrics/noop.d.ts.map +1 -1
  82. package/dist/metrics/noop.js +4 -1
  83. package/dist/metrics/recorder.d.ts.map +1 -1
  84. package/dist/metrics/recorder.js +11 -8
  85. package/dist/metrics/types.d.ts +31 -5
  86. package/dist/metrics/types.d.ts.map +1 -1
  87. package/dist/metrics/utils/html-report.utils.d.ts +2 -1
  88. package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
  89. package/dist/metrics/utils/html-report.utils.js +1115 -1136
  90. package/dist/types/config.d.ts +18 -6
  91. package/dist/types/config.d.ts.map +1 -1
  92. package/dist/types/content.d.ts +83 -16
  93. package/dist/types/content.d.ts.map +1 -1
  94. package/dist/types/index.d.ts +4 -3
  95. package/dist/types/index.d.ts.map +1 -1
  96. package/dist/types/isg.d.ts +48 -2
  97. package/dist/types/isg.d.ts.map +1 -1
  98. package/package.json +6 -6
  99. package/dist/core/utils/callable-partials.utils.d.ts +0 -60
  100. package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
  101. package/dist/core/utils/callable-partials.utils.js +0 -127
  102. package/dist/core/utils/partial-validation.utils.d.ts +0 -9
  103. package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
  104. package/dist/core/utils/partial-validation.utils.js +0 -107
package/README.md CHANGED
@@ -260,12 +260,9 @@ export default defineConfig({
260
260
  },
261
261
  },
262
262
 
263
- // Eta template engine
264
- eta: {
265
- filters: {
266
- formatDate: (date) => new Date(date).toLocaleDateString('en-US'),
267
- slugify: (text) => text.toLowerCase().replace(/\s+/g, '-'),
268
- },
263
+ // Template helpers, called as stati.helpers.formatDate()
264
+ helpers: {
265
+ formatDate: (date) => new Date(date).toLocaleDateString('en-US'),
269
266
  },
270
267
 
271
268
  // Incremental Static Generation (enabled by default)
@@ -588,7 +585,7 @@ import type { TemplateContext, CacheManifest } from '@stati/core/types';
588
585
  Stati supports rich Markdown features out of the box:
589
586
 
590
587
  - **Front-matter** - YAML (default) or JSON (`---json`) metadata
591
- - **Plugins** - Full markdown-it ecosystem compatibility
588
+ - **Plugins** - Any markdown-it plugin that supports markdown-it 15
592
589
  - **Syntax highlighting** - Code blocks get `language-*` classes; add a plugin such as `markdown-it-prism` to highlight them
593
590
  - **Custom rendering** - Configure parser behavior
594
591
  - **Draft mode** - Mark pages as `draft: true`
@@ -611,10 +608,10 @@ export default defineConfig({
611
608
 
612
609
  Powered by [Eta](https://eta.js.org) for fast, flexible templates:
613
610
 
614
- - **Layouts** - Template inheritance via `layout` property
615
- - **Partials** - Reusable components
616
- - **Custom filters** - Transform data in templates
617
- - **Type-safe helpers** - Access page data with autocomplete
611
+ - **Layouts** - Chosen by folder or front matter, sharing one page shell through `stati.layout()`
612
+ - **Includes** - Reusable templates with props and content through `stati.include()`
613
+ - **Helpers** - Your own functions, called as `stati.helpers.name()`
614
+ - **Typed context** - The `TemplateContext` type describes everything templates can read
618
615
  - **Live reload** - The browser reloads when a template changes
619
616
 
620
617
  **Template structure:**
@@ -627,11 +624,11 @@ Powered by [Eta](https://eta.js.org) for fast, flexible templates:
627
624
  <title><%= stati.page.title %> - <%= stati.site.title %></title>
628
625
  </head>
629
626
  <body>
630
- <%~ stati.partials.header %>
627
+ <%~ stati.include('partials/header') %>
631
628
  <main>
632
629
  <%~ stati.content %>
633
630
  </main>
634
- <%~ stati.partials.footer %>
631
+ <%~ stati.include('partials/footer') %>
635
632
  </body>
636
633
  </html>
637
634
  ```
@@ -3,6 +3,15 @@ import type { StatiConfig } from '../types/index.js';
3
3
  * Clear the config cache. Useful when config file changes are detected.
4
4
  */
5
5
  export declare function clearConfigCache(): void;
6
+ /**
7
+ * Checks the top-level options: each must be one Stati knows, and `helpers` must map names to
8
+ * functions.
9
+ *
10
+ * @param userConfig - The configuration as the config file exports it
11
+ * @throws {ConfigValidationError} For an unknown option, naming similar ones, or `helpers` that
12
+ * are not an object of functions
13
+ */
14
+ export declare function validateConfigOptions(userConfig: Record<string, unknown>): void;
6
15
  /**
7
16
  * Builds config file paths for a given directory.
8
17
  * @param cwd - Directory to search for config files
@@ -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;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"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmCrD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqDD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA6B/E;AAED;;;;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,CA6IlF"}
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
5
5
  import { validateImagesConfig } from '../images/config.js';
6
6
  import { ConfigValidationError } from './errors.js';
7
- import { compileStatiConfig, cleanupCompiledConfig } from '../core/utils/index.js';
7
+ import { compileStatiConfig, cleanupCompiledConfig, findSimilarNames, } from '../core/utils/index.js';
8
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';
9
9
  /**
10
10
  * Config cache to prevent repeated dynamic imports during dev server rebuilds.
@@ -35,6 +35,70 @@ const DEFAULT_CONFIG = {
35
35
  maxAgeCapDays: DEFAULT_MAX_AGE_CAP_DAYS,
36
36
  },
37
37
  };
38
+ /**
39
+ * Every top-level `StatiConfig` option. Typed as a Record so a new option fails to compile
40
+ * until it is added.
41
+ */
42
+ const CONFIG_KEYS = {
43
+ srcDir: true,
44
+ outDir: true,
45
+ staticDir: true,
46
+ site: true,
47
+ markdown: true,
48
+ helpers: true,
49
+ isg: true,
50
+ collections: true,
51
+ seo: true,
52
+ sitemap: true,
53
+ robots: true,
54
+ rss: true,
55
+ search: true,
56
+ typescript: true,
57
+ images: true,
58
+ dev: true,
59
+ preview: true,
60
+ hooks: true,
61
+ };
62
+ function describeConfigValue(value) {
63
+ if (value === null || value === undefined)
64
+ return String(value);
65
+ if (Array.isArray(value))
66
+ return 'an array';
67
+ if (typeof value === 'object')
68
+ return 'an object';
69
+ return typeof value === 'string' ? `string '${value}'` : `${typeof value} ${String(value)}`;
70
+ }
71
+ /**
72
+ * Checks the top-level options: each must be one Stati knows, and `helpers` must map names to
73
+ * functions.
74
+ *
75
+ * @param userConfig - The configuration as the config file exports it
76
+ * @throws {ConfigValidationError} For an unknown option, naming similar ones, or `helpers` that
77
+ * are not an object of functions
78
+ */
79
+ export function validateConfigOptions(userConfig) {
80
+ const known = Object.keys(CONFIG_KEYS);
81
+ for (const key of Object.keys(userConfig)) {
82
+ if (!Object.hasOwn(CONFIG_KEYS, key)) {
83
+ const similar = findSimilarNames(key, known);
84
+ throw new ConfigValidationError(similar.length > 0
85
+ ? `Unknown option ${key}. Did you mean ${similar.join(' or ')}?`
86
+ : `Unknown option ${key}. Allowed options: ${known.join(', ')}.`);
87
+ }
88
+ }
89
+ const helpers = userConfig.helpers;
90
+ if (helpers === undefined) {
91
+ return;
92
+ }
93
+ if (typeof helpers !== 'object' || helpers === null || Array.isArray(helpers)) {
94
+ throw new ConfigValidationError(`helpers must be an object of functions, such as helpers: { formatDate: (date) => ... } (got ${describeConfigValue(helpers)}).`);
95
+ }
96
+ for (const [name, helper] of Object.entries(helpers)) {
97
+ if (typeof helper !== 'function') {
98
+ throw new ConfigValidationError(`helpers.${name} must be a function that templates call as stati.helpers.${name}() (got ${describeConfigValue(helper)}).`);
99
+ }
100
+ }
101
+ }
38
102
  /**
39
103
  * Builds config file paths for a given directory.
40
104
  * @param cwd - Directory to search for config files
@@ -151,6 +215,18 @@ export async function loadConfig(cwd = process.cwd()) {
151
215
  }
152
216
  throw error;
153
217
  }
218
+ // Unknown options and helpers
219
+ try {
220
+ validateConfigOptions(userConfig);
221
+ }
222
+ catch (error) {
223
+ if (error instanceof ConfigValidationError) {
224
+ throw new ConfigValidationError(`Invalid configuration in ${configPath}:\n${error.message}`, {
225
+ cause: error,
226
+ });
227
+ }
228
+ throw error;
229
+ }
154
230
  // Cache the loaded config with its modification time
155
231
  try {
156
232
  const mtime = statSync(resolvedConfigPath).mtimeMs;
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAyEA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAQP,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"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA0EA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAQP,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,CAqB5E"}
@@ -1,4 +1,4 @@
1
- import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, removeStaleBundleFiles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, } from './utils/index.js';
1
+ import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, removeStaleBundleFiles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, resolveSrcDir, } from './utils/index.js';
2
2
  import { join, dirname, relative, posix } from 'node:path';
3
3
  import { performance } from 'node:perf_hooks';
4
4
  import { loadConfig } from '../config/loader.js';
@@ -7,8 +7,9 @@ import { expandPages } from './expand.js';
7
7
  import { createSummaryHasher, resolveCollectionPath, resolveSummaryFields, } from './collections.js';
8
8
  import { createMarkdownProcessor, renderMarkdown, extractToc } from './markdown.js';
9
9
  import { createTemplateEngine, renderPage } from './templates.js';
10
+ import { loadTemplateIndex } from './template-index.js';
10
11
  import { buildNavigation } from './navigation.js';
11
- import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
12
+ import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, } from './isg/index.js';
12
13
  import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
13
14
  import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
14
15
  import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, removeStaleSearchIndexes, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
@@ -139,7 +140,6 @@ export async function build(options = {}) {
139
140
  // Clear per-build caches at the start of each build
140
141
  // This prevents stale data from previous builds and ensures consistent state
141
142
  clearFileHashCache();
142
- clearTemplatePathCache();
143
143
  // Ensure cache directory exists before acquiring build lock
144
144
  await ensureDir(cacheDir);
145
145
  // In dev mode, bypass file-based build lock (dev server handles concurrency via isBuildingRef
@@ -229,8 +229,7 @@ async function writeSearchIndexBeforeRendering(pages, config, md, outDir, logger
229
229
  const isDevMode = isDevelopment();
230
230
  const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
231
231
  // TOC entries and markdown content, the same data a render produces, without rendering.
232
- // Sorted by URL: content discovery returns files in a different order on each run, which
233
- // would change the index content, and with it the name, on every build.
232
+ // Sorted by URL, so the index content, and with it the name, changes only when pages do.
234
233
  const tocEnabled = config.markdown?.toc !== false;
235
234
  const byUrl = (a, b) => a.url.localeCompare(b.url) || a.sourcePath.localeCompare(b.sourcePath);
236
235
  const searchablePages = pages
@@ -290,9 +289,11 @@ function hashableData(value) {
290
289
  /**
291
290
  * Processes pages with ISG caching logic.
292
291
  */
293
- async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
292
+ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, imageRuntime = null, navigationHash = computeNavigationHash(navigation), templateIndex) {
294
293
  let cacheHits = 0;
295
294
  let cacheMisses = 0;
295
+ // Every .eta file under the source directory, read once for all pages
296
+ const index = templateIndex ?? (await loadTemplateIndex(resolveSrcDir(config)));
296
297
  // Build context
297
298
  const buildContext = { config, pages };
298
299
  // Run beforeAll hook
@@ -326,7 +327,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
326
327
  }
327
328
  // Track timing for shouldRebuildPage
328
329
  let totalShouldRebuildTime = 0;
330
+ let totalMarkdownTime = 0;
329
331
  let totalRenderTime = 0;
332
+ let totalPostRenderTime = 0;
330
333
  let totalFileWriteTime = 0;
331
334
  let totalCacheEntryTime = 0;
332
335
  // Batch pending writes for parallel execution (dev mode optimization)
@@ -402,7 +405,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
402
405
  (existingEntry !== undefined && !readsKnown) ||
403
406
  // A cached page whose generated images are gone is rebuilt to regenerate them
404
407
  (imageRuntime !== null && !imageRuntime.outputsExist(existingEntry?.images)) ||
405
- (await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
408
+ (await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash, index));
406
409
  const shouldRebuildTime = performance.now() - shouldRebuildStart;
407
410
  totalShouldRebuildTime += shouldRebuildTime;
408
411
  if (!shouldRebuild) {
@@ -427,18 +430,23 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
427
430
  // Image state for this page: markup, dependencies and generated files
428
431
  const pageImages = imageRuntime?.createPageContext(page);
429
432
  // Render markdown to HTML with TOC extraction
433
+ const markdownStart = performance.now();
430
434
  const tocEnabled = config.markdown?.toc !== false;
431
435
  const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled, {
432
436
  images: pageImages,
433
437
  });
438
+ totalMarkdownTime += performance.now() - markdownStart;
434
439
  // Render with template
435
440
  const renderPageStart = performance.now();
436
- const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger, { images: pageImages });
441
+ const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger, { images: pageImages, templateIndex: index });
437
442
  const renderPageTime = performance.now() - renderPageStart;
438
443
  totalRenderTime += renderPageTime;
444
+ const postRenderStart = performance.now();
439
445
  let finalHtml = renderResult.html;
440
- // Record templates loaded for this page
446
+ // Record the templates this page used and how often templates rendered
441
447
  recorder.increment('templatesLoaded', renderResult.templatesLoaded);
448
+ recorder.increment('templateRenders', renderResult.templateRenders);
449
+ recorder.increment('includeCacheHits', renderResult.includeCacheHits);
442
450
  // Auto-inject SEO tags if enabled
443
451
  if (config.seo?.autoInject !== false) {
444
452
  const injectOptions = {
@@ -474,13 +482,16 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
474
482
  assetDeps: committedImages?.assetDeps ?? [],
475
483
  images: committedImages?.images ?? [],
476
484
  reads: renderResult.reads,
485
+ templates: renderResult.templates,
486
+ templateLookups: renderResult.templateLookups,
477
487
  };
478
488
  const renderedInputsHash = renderInputsHashFor(renderResult.reads);
479
- const renderTime = Math.round(performance.now() - startTime);
489
+ totalPostRenderTime += performance.now() - postRenderStart;
490
+ const renderTime = performance.now() - startTime;
480
491
  // Record page timing for rendered pages (includes template count)
481
- recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
492
+ recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded, renderResult.templateRenders);
482
493
  if (logger.updateProgress) {
483
- logger.updateProgress('rendered', page.url, renderTime);
494
+ logger.updateProgress('rendered', page.url, Math.round(renderTime));
484
495
  }
485
496
  // In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
486
497
  if (isDevMode) {
@@ -502,12 +513,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
502
513
  // Update cache manifest
503
514
  if (!options.skipCacheUpdate) {
504
515
  const cacheEntryStart = performance.now();
505
- if (existingEntry) {
506
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderedInputsHash, renderedAssets);
507
- }
508
- else {
509
- manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderedInputsHash, renderedAssets);
510
- }
516
+ manifest.entries[cacheKey] = existingEntry
517
+ ? await updateCacheEntry(existingEntry, page, config, buildTime, renderedInputsHash, renderedAssets)
518
+ : await createCacheEntry(page, config, buildTime, renderedInputsHash, renderedAssets);
511
519
  totalCacheEntryTime += performance.now() - cacheEntryStart;
512
520
  }
513
521
  }
@@ -527,26 +535,22 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
527
535
  // Write all files in parallel
528
536
  await Promise.all(pendingWrites.map((w) => writeFile(w.outputPath, w.content, 'utf-8')));
529
537
  totalFileWriteTime = performance.now() - fileWriteStart;
530
- // Update cache entries SEQUENTIALLY with fast update (reuse existing deps)
531
- // This avoids expensive template dependency tracking on each incremental rebuild
538
+ // Update cache entries sequentially, from the templates each render recorded
532
539
  if (!options.skipCacheUpdate) {
533
540
  const cacheEntryStart = performance.now();
534
541
  for (const { cacheKey, page, existingEntry, renderInputsHash, renderedAssets, } of pendingCacheUpdates) {
535
- if (existingEntry) {
536
- // Fast update: reuse existing deps, just update hashes and timestamp
537
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, true, // fastUpdate = true in dev mode
538
- renderInputsHash, renderedAssets);
539
- }
540
- else {
541
- manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash, renderedAssets);
542
- }
542
+ manifest.entries[cacheKey] = existingEntry
543
+ ? await updateCacheEntry(existingEntry, page, config, buildTime, renderInputsHash, renderedAssets)
544
+ : await createCacheEntry(page, config, buildTime, renderInputsHash, renderedAssets);
543
545
  }
544
546
  totalCacheEntryTime = performance.now() - cacheEntryStart;
545
547
  }
546
548
  }
547
549
  // Record detailed phase timings in metrics
548
550
  recorder.recordPhase('shouldRebuildTotalMs', totalShouldRebuildTime);
551
+ recorder.recordPhase('markdownRenderTotalMs', totalMarkdownTime);
549
552
  recorder.recordPhase('renderPageTotalMs', totalRenderTime);
553
+ recorder.recordPhase('postRenderTotalMs', totalPostRenderTime);
550
554
  recorder.recordPhase('fileWriteTotalMs', totalFileWriteTime);
551
555
  recorder.recordPhase('cacheEntryTotalMs', totalCacheEntryTime);
552
556
  // Display final progress summary
@@ -732,9 +736,15 @@ async function buildInternal(options = {}) {
732
736
  if (logger.step) {
733
737
  console.log(); // Add spacing before page processing
734
738
  }
739
+ // Every template under the source directory, read once per build; a dev server session
740
+ // keeps one between rebuilds and updates it as templates are added and deleted
741
+ const endTemplateIndexSpan = recorder.startSpan('templateIndexMs');
742
+ const templateIndex = await loadTemplateIndex(resolveSrcDir(config));
743
+ endTemplateIndexSpan();
744
+ recorder.increment('templatesIndexed', templateIndex.size);
735
745
  const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
736
746
  const buildTime = new Date();
737
- const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, imageRuntime, navigationHash);
747
+ const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, imageRuntime, navigationHash, templateIndex);
738
748
  endPageRenderSpan();
739
749
  const { cacheHits, cacheMisses, searchIndexFilename } = pageProcessingResult;
740
750
  // Remove the output of pages that no longer exist, such as a deleted post or a tag no post
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/core/content.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,CAiDtB"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/core/content.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,CAsDtB"}
@@ -1,6 +1,5 @@
1
- import glob from 'fast-glob';
2
- import { readFile, resolveSrcDir, getFrontMatterDate } from './utils/index.js';
3
- import matter from 'gray-matter';
1
+ import { glob } from 'tinyglobby';
2
+ import { readFile, resolveSrcDir, getFrontMatterDate, parseFrontMatter } from './utils/index.js';
4
3
  import { relative, dirname, basename } from 'node:path';
5
4
  import { MARKDOWN_EXTENSION } from '../constants.js';
6
5
  /**
@@ -21,12 +20,15 @@ import { MARKDOWN_EXTENSION } from '../constants.js';
21
20
  */
22
21
  export async function loadContent(config, includeDrafts) {
23
22
  const contentDir = resolveSrcDir(config);
24
- // Exclude folders starting with underscore from content discovery
25
- const files = await glob('**/*.md', {
23
+ // Exclude folders starting with underscore from content discovery. Sorted, because the
24
+ // file system lists files in no fixed order, and pages that sort equally in listings and
25
+ // feeds keep the order they were found in.
26
+ const files = (await glob('**/*.md', {
26
27
  cwd: contentDir,
27
28
  absolute: true,
28
29
  ignore: ['**/_*/**', '_*/**'],
29
- });
30
+ expandDirectories: false,
31
+ })).sort();
30
32
  const pages = [];
31
33
  for (const file of files) {
32
34
  const content = await readFile(file, 'utf-8');
@@ -34,7 +36,7 @@ export async function loadContent(config, includeDrafts) {
34
36
  console.warn(`Skipping file ${file}: could not read content`);
35
37
  continue;
36
38
  }
37
- const { data: frontMatter, content: markdown } = matter(content);
39
+ const { data: frontMatter, content: markdown } = parseFrontMatter(content, file);
38
40
  // Skip drafts unless explicitly included
39
41
  if (frontMatter.draft && !includeDrafts) {
40
42
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAoC7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAmZD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmhBxF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA4C7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyaD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAwdxF"}