@stati/core 1.27.2 → 1.29.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 (136) hide show
  1. package/README.md +6 -1
  2. package/dist/config/errors.d.ts +10 -0
  3. package/dist/config/errors.d.ts.map +1 -0
  4. package/dist/config/errors.js +10 -0
  5. package/dist/config/loader.d.ts.map +1 -1
  6. package/dist/config/loader.js +15 -2
  7. package/dist/constants.d.ts +26 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +23 -0
  10. package/dist/core/build.d.ts.map +1 -1
  11. package/dist/core/build.js +201 -41
  12. package/dist/core/collections.d.ts +139 -0
  13. package/dist/core/collections.d.ts.map +1 -0
  14. package/dist/core/collections.js +342 -0
  15. package/dist/core/content.d.ts.map +1 -1
  16. package/dist/core/content.js +2 -1
  17. package/dist/core/dev.d.ts.map +1 -1
  18. package/dist/core/dev.js +11 -62
  19. package/dist/core/expand.d.ts +86 -0
  20. package/dist/core/expand.d.ts.map +1 -0
  21. package/dist/core/expand.js +484 -0
  22. package/dist/core/isg/builder.d.ts +27 -2
  23. package/dist/core/isg/builder.d.ts.map +1 -1
  24. package/dist/core/isg/builder.js +83 -11
  25. package/dist/core/isg/deps.d.ts +9 -0
  26. package/dist/core/isg/deps.d.ts.map +1 -1
  27. package/dist/core/isg/deps.js +30 -1
  28. package/dist/core/isg/hash.d.ts +14 -2
  29. package/dist/core/isg/hash.d.ts.map +1 -1
  30. package/dist/core/isg/hash.js +30 -2
  31. package/dist/core/isg/index.d.ts +4 -2
  32. package/dist/core/isg/index.d.ts.map +1 -1
  33. package/dist/core/isg/index.js +4 -2
  34. package/dist/core/isg/manifest.js +14 -0
  35. package/dist/core/isg/prune.d.ts +32 -0
  36. package/dist/core/isg/prune.d.ts.map +1 -0
  37. package/dist/core/isg/prune.js +113 -0
  38. package/dist/core/markdown.d.ts +10 -1
  39. package/dist/core/markdown.d.ts.map +1 -1
  40. package/dist/core/markdown.js +13 -3
  41. package/dist/core/navigation.d.ts +4 -2
  42. package/dist/core/navigation.d.ts.map +1 -1
  43. package/dist/core/navigation.js +17 -2
  44. package/dist/core/templates.d.ts +15 -1
  45. package/dist/core/templates.d.ts.map +1 -1
  46. package/dist/core/templates.js +121 -126
  47. package/dist/core/utils/fs.utils.d.ts +29 -1
  48. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  49. package/dist/core/utils/fs.utils.js +74 -1
  50. package/dist/core/utils/index.d.ts +3 -3
  51. package/dist/core/utils/index.d.ts.map +1 -1
  52. package/dist/core/utils/index.js +3 -3
  53. package/dist/core/utils/paths.utils.d.ts +15 -0
  54. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  55. package/dist/core/utils/paths.utils.js +20 -0
  56. package/dist/core/utils/template-discovery.utils.d.ts +25 -1
  57. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  58. package/dist/core/utils/template-discovery.utils.js +61 -11
  59. package/dist/images/config.d.ts +52 -0
  60. package/dist/images/config.d.ts.map +1 -0
  61. package/dist/images/config.js +200 -0
  62. package/dist/images/index.d.ts +15 -0
  63. package/dist/images/index.d.ts.map +1 -0
  64. package/dist/images/index.js +11 -0
  65. package/dist/images/manifest.d.ts +36 -0
  66. package/dist/images/manifest.d.ts.map +1 -0
  67. package/dist/images/manifest.js +69 -0
  68. package/dist/images/markdown-rule.d.ts +26 -0
  69. package/dist/images/markdown-rule.d.ts.map +1 -0
  70. package/dist/images/markdown-rule.js +60 -0
  71. package/dist/images/markup.d.ts +39 -0
  72. package/dist/images/markup.d.ts.map +1 -0
  73. package/dist/images/markup.js +72 -0
  74. package/dist/images/og.d.ts +52 -0
  75. package/dist/images/og.d.ts.map +1 -0
  76. package/dist/images/og.js +165 -0
  77. package/dist/images/plan.d.ts +101 -0
  78. package/dist/images/plan.d.ts.map +1 -0
  79. package/dist/images/plan.js +128 -0
  80. package/dist/images/provider.d.ts +35 -0
  81. package/dist/images/provider.d.ts.map +1 -0
  82. package/dist/images/provider.js +70 -0
  83. package/dist/images/resolve.d.ts +45 -0
  84. package/dist/images/resolve.d.ts.map +1 -0
  85. package/dist/images/resolve.js +51 -0
  86. package/dist/images/runtime.d.ts +258 -0
  87. package/dist/images/runtime.d.ts.map +1 -0
  88. package/dist/images/runtime.js +659 -0
  89. package/dist/images/sources.d.ts +47 -0
  90. package/dist/images/sources.d.ts.map +1 -0
  91. package/dist/images/sources.js +97 -0
  92. package/dist/images/utils/image-probe.utils.d.ts +30 -0
  93. package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
  94. package/dist/images/utils/image-probe.utils.js +307 -0
  95. package/dist/images/utils/index.d.ts +7 -0
  96. package/dist/images/utils/index.d.ts.map +1 -0
  97. package/dist/images/utils/index.js +5 -0
  98. package/dist/index.d.ts +2 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/metrics/noop.d.ts.map +1 -1
  101. package/dist/metrics/noop.js +3 -0
  102. package/dist/metrics/recorder.d.ts.map +1 -1
  103. package/dist/metrics/recorder.js +3 -0
  104. package/dist/metrics/types.d.ts +8 -0
  105. package/dist/metrics/types.d.ts.map +1 -1
  106. package/dist/rss/generator.d.ts.map +1 -1
  107. package/dist/rss/generator.js +5 -2
  108. package/dist/search/generator.d.ts +9 -0
  109. package/dist/search/generator.d.ts.map +1 -1
  110. package/dist/search/generator.js +20 -0
  111. package/dist/search/index.d.ts +1 -1
  112. package/dist/search/index.d.ts.map +1 -1
  113. package/dist/search/index.js +1 -1
  114. package/dist/seo/auto-inject.d.ts +3 -0
  115. package/dist/seo/auto-inject.d.ts.map +1 -1
  116. package/dist/seo/auto-inject.js +1 -0
  117. package/dist/seo/generator.d.ts +24 -0
  118. package/dist/seo/generator.d.ts.map +1 -1
  119. package/dist/seo/generator.js +65 -1
  120. package/dist/seo/index.d.ts +1 -1
  121. package/dist/seo/index.d.ts.map +1 -1
  122. package/dist/seo/index.js +1 -1
  123. package/dist/types/config.d.ts +60 -0
  124. package/dist/types/config.d.ts.map +1 -1
  125. package/dist/types/content.d.ts +150 -3
  126. package/dist/types/content.d.ts.map +1 -1
  127. package/dist/types/images.d.ts +185 -0
  128. package/dist/types/images.d.ts.map +1 -0
  129. package/dist/types/images.js +8 -0
  130. package/dist/types/index.d.ts +3 -2
  131. package/dist/types/index.d.ts.map +1 -1
  132. package/dist/types/isg.d.ts +19 -0
  133. package/dist/types/isg.d.ts.map +1 -1
  134. package/dist/types/seo.d.ts +9 -0
  135. package/dist/types/seo.d.ts.map +1 -1
  136. package/package.json +1 -1
@@ -2,8 +2,10 @@ import type { PageModel, NavNode } from '../types/index.js';
2
2
  /**
3
3
  * Builds a hierarchical navigation structure from pages.
4
4
  * Groups pages by directory structure and sorts them appropriately.
5
+ * Generated pages are left out unless their template sets `nav: true`, and so are the
6
+ * later pages of paginated listings.
5
7
  *
6
- * @param pages - Array of page models to build navigation from
8
+ * @param allPages - Array of page models to build navigation from
7
9
  * @returns Array of top-level navigation nodes
8
10
  *
9
11
  * @example
@@ -17,5 +19,5 @@ import type { PageModel, NavNode } from '../types/index.js';
17
19
  * // Results in hierarchical structure with sorted blog posts
18
20
  * ```
19
21
  */
20
- export declare function buildNavigation(pages: PageModel[]): NavNode[];
22
+ export declare function buildNavigation(allPages: PageModel[]): NavNode[];
21
23
  //# sourceMappingURL=navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAyC7D"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAe5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CA2ChE"}
@@ -1,9 +1,23 @@
1
1
  import { basename } from 'node:path';
2
+ import { isVirtualPage } from './utils/index.js';
3
+ /**
4
+ * Whether a page appears in navigation. Pages generated from a `[param].md` template appear
5
+ * only when the template sets `nav: true`; later pages of a paginated listing never do.
6
+ */
7
+ function isInNavigation(page) {
8
+ if (!isVirtualPage(page)) {
9
+ return true;
10
+ }
11
+ const firstPage = (page.pagination?.pageNumber ?? 1) === 1;
12
+ return page.generated !== undefined && firstPage && page.frontMatter.nav === true;
13
+ }
2
14
  /**
3
15
  * Builds a hierarchical navigation structure from pages.
4
16
  * Groups pages by directory structure and sorts them appropriately.
17
+ * Generated pages are left out unless their template sets `nav: true`, and so are the
18
+ * later pages of paginated listings.
5
19
  *
6
- * @param pages - Array of page models to build navigation from
20
+ * @param allPages - Array of page models to build navigation from
7
21
  * @returns Array of top-level navigation nodes
8
22
  *
9
23
  * @example
@@ -17,7 +31,8 @@ import { basename } from 'node:path';
17
31
  * // Results in hierarchical structure with sorted blog posts
18
32
  * ```
19
33
  */
20
- export function buildNavigation(pages) {
34
+ export function buildNavigation(allPages) {
35
+ const pages = allPages.filter(isInNavigation);
21
36
  // Group pages by their collection (directory)
22
37
  const collections = new Map();
23
38
  for (const page of pages) {
@@ -1,5 +1,8 @@
1
1
  import { Eta } from 'eta';
2
2
  import type { StatiConfig, PageModel, NavNode, TocEntry, Logger } from '../types/index.js';
3
+ import type { PageReads } from './collections.js';
4
+ import type { PageImages } from '../images/runtime.js';
5
+ import type { GeneratedOgImage } from '../types/index.js';
3
6
  /**
4
7
  * Result of rendering a page template.
5
8
  */
@@ -8,6 +11,17 @@ export interface RenderResult {
8
11
  html: string;
9
12
  /** Number of templates loaded (layout + partials) */
10
13
  templatesLoaded: number;
14
+ /** Open Graph image generated for the page, if any */
15
+ ogImage?: GeneratedOgImage | undefined;
16
+ /** Shared data the render read, which the cache entry records */
17
+ reads: PageReads;
18
+ }
19
+ /**
20
+ * Page-specific state for {@link renderPage}.
21
+ */
22
+ export interface RenderPageExtras {
23
+ /** Image state of the page; without it `stati.image()` emits a plain `<img>` */
24
+ images?: PageImages | undefined;
11
25
  }
12
26
  /**
13
27
  * Clear the cached template engine. Call this when templates change in dev mode
@@ -15,5 +29,5 @@ export interface RenderResult {
15
29
  */
16
30
  export declare function clearTemplateEngineCache(): void;
17
31
  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>;
32
+ 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
33
  //# 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;AAuB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,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;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAuHD;;;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,CA8RvB"}
@@ -3,122 +3,48 @@ import { join, dirname, relative, basename, posix } from 'node:path';
3
3
  import glob from 'fast-glob';
4
4
  import { createFallbackLogger } from './utils/index.js';
5
5
  import { TEMPLATE_EXTENSION } from '../constants.js';
6
- import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
6
+ import { getStatiVersion, isCollectionIndexPage, discoverLayout, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, slugify, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
7
+ import { getCollections, resolveCollectionPath, createCollectionView, copyPaginationData, copyGeneratedData, } from './collections.js';
7
8
  import { isDevelopment, isProduction } from '../env.js';
8
- import { generateSEO } from '../seo/index.js';
9
+ import { generateSEO, resolveGeneratedImagePlaceholders } from '../seo/index.js';
9
10
  /**
10
- * Groups pages by their tags for aggregation purposes.
11
- *
12
- * @param pages - Pages to group
13
- * @returns Object mapping tag names to arrays of pages
11
+ * `stati.image()` when images are turned off: a plain `<img>`, attributes escaped.
14
12
  */
15
- function groupPagesByTags(pages) {
16
- const pagesByTag = {};
17
- for (const page of pages) {
18
- const tags = page.frontMatter.tags;
19
- if (Array.isArray(tags)) {
20
- for (const tag of tags) {
21
- if (typeof tag === 'string') {
22
- if (!pagesByTag[tag]) {
23
- pagesByTag[tag] = [];
24
- }
25
- pagesByTag[tag].push(page);
26
- }
27
- }
28
- }
13
+ function renderPlainImage(src, options = {}) {
14
+ if (typeof src !== 'string' || !src.trim()) {
15
+ return '';
29
16
  }
30
- return pagesByTag;
31
- }
32
- /**
33
- * Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
34
- *
35
- * @param pages - Pages to sort
36
- * @returns Sorted array of pages
37
- */
38
- function sortPagesByDate(pages) {
39
- return pages.sort((a, b) => {
40
- // Sort by publishedAt (newest first)
41
- if (a.publishedAt && b.publishedAt) {
42
- return b.publishedAt.getTime() - a.publishedAt.getTime();
43
- }
44
- if (a.publishedAt && !b.publishedAt)
45
- return -1;
46
- if (!a.publishedAt && b.publishedAt)
47
- return 1;
48
- // Fallback to title
49
- const titleA = a.frontMatter.title || basename(a.url) || 'Untitled';
50
- const titleB = b.frontMatter.title || basename(b.url) || 'Untitled';
51
- return titleA.localeCompare(titleB);
52
- });
17
+ const attributes = [
18
+ `src="${escapeHtml(src)}"`,
19
+ `alt="${escapeHtml(options.alt ?? '')}"`,
20
+ ...(options.title ? [`title="${escapeHtml(options.title)}"`] : []),
21
+ ...(options.class ? [`class="${escapeHtml(options.class)}"`] : []),
22
+ ];
23
+ return `<img ${attributes.join(' ')}>`;
53
24
  }
54
25
  /**
55
- * Builds collection aggregation data for index pages.
56
- * Provides all the data needed for index pages to list and organize their collection's content.
57
- *
58
- * @param currentPage - The current page being rendered (must be an index page)
59
- * @param allPages - All pages in the site
60
- * @returns Collection data for template rendering
26
+ * Wraps the navigation helpers for one render so that reading any of them sets `reads.nav`.
27
+ * Like the collection view, it uses accessors on a plain object, so spreading the template
28
+ * context does not count as a read. The helpers return the shared nodes unchanged.
61
29
  */
62
- function buildCollectionData(currentPage, allPages) {
63
- const collectionPath = currentPage.url === '/' ? '/' : currentPage.url;
64
- const collectionName = collectionPath === '/' ? 'Home' : basename(collectionPath);
65
- // Find all pages that belong to this collection
66
- const collectionPages = allPages.filter((page) => {
67
- if (page.url === currentPage.url) {
68
- return false; // Don't include the index page itself
69
- }
70
- if (collectionPath === '/') {
71
- // For root collection, include pages that are direct children of root
72
- const pageCollectionPath = getCollectionPathForPage(page.url);
73
- return pageCollectionPath === '/';
74
- }
75
- else {
76
- // For other collections, include pages that start with the collection path
77
- const pageUrlAsDir = collectionPath.endsWith('/') ? collectionPath : collectionPath + '/';
78
- return page.url.startsWith(pageUrlAsDir);
79
- }
80
- });
81
- // Find direct children (one level down)
82
- const children = collectionPages.filter((page) => {
83
- const relativePath = page.url.substring(collectionPath.length);
84
- const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
85
- return !cleanPath.includes('/'); // No further slashes means direct child
86
- });
87
- // Sort pages by date for recent posts
88
- const recentPages = sortPagesByDate([...collectionPages]);
89
- // Group by tags
90
- const pagesByTag = groupPagesByTags(collectionPages);
91
- return {
92
- pages: collectionPages,
93
- children,
94
- recentPages,
95
- pagesByTag,
96
- metadata: {
97
- totalPages: collectionPages.length,
98
- hasChildren: children.length > 0,
99
- collectionPath,
100
- collectionName,
101
- },
102
- };
103
- }
104
- /**
105
- * Builds collection data for a child page (showing parent collection data).
106
- * Finds the parent collection and returns its data.
107
- *
108
- * @param currentPage - The current page being rendered
109
- * @param allPages - All pages in the site
110
- * @returns Collection data for the parent collection or undefined
111
- */
112
- function buildParentCollectionData(currentPage, allPages) {
113
- // Get the parent collection path
114
- const parentPath = getCollectionPathForPage(currentPage.url);
115
- // Find the index page for the parent collection
116
- const parentIndexPage = allPages.find((p) => p.url === parentPath);
117
- if (parentIndexPage) {
118
- // Build collection data for the parent
119
- return buildCollectionData(parentIndexPage, allPages);
30
+ function createNavView(helpers, reads) {
31
+ const view = {};
32
+ for (const [key, initial] of Object.entries(helpers)) {
33
+ let value = initial;
34
+ Object.defineProperty(view, key, {
35
+ enumerable: true,
36
+ configurable: true,
37
+ get() {
38
+ reads.nav = true;
39
+ return value;
40
+ },
41
+ set(next) {
42
+ reads.nav = true;
43
+ value = next;
44
+ },
45
+ });
120
46
  }
121
- return undefined;
47
+ return view;
122
48
  }
123
49
  /**
124
50
  * Discovers partials in the hierarchy for a given page path.
@@ -215,25 +141,34 @@ export function createTemplateEngine(config) {
215
141
  }
216
142
  return eta;
217
143
  }
218
- export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger) {
144
+ export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger, extras) {
219
145
  const log = logger || createFallbackLogger();
146
+ const pageImages = extras?.images;
147
+ // The generated Open Graph image is known once the og template has rendered, after the
148
+ // partials. Until then stati.generateSEO() emits placeholders for its tags.
149
+ const ogState = {
150
+ image: undefined,
151
+ pending: pageImages?.wantsOg() ?? false,
152
+ };
220
153
  // Discover partials for this page's directory hierarchy
221
154
  const srcDir = resolveSrcDir(config);
222
155
  const relativePath = relative(srcDir, page.sourcePath);
223
156
  const partialPaths = await discoverPartials(relativePath, config);
224
157
  // Count templates: partials + layout (if found)
225
158
  const partialsCount = Object.keys(partialPaths).length;
226
- // Build collection data based on page type
159
+ // What this render reads from shared data, so the cache can rebuild the page when it changes
160
+ const reads = { collection: false, nav: false };
161
+ // The page's collection: its own on index pages, the one it belongs to on other pages, or
162
+ // the one its front matter names. Collections are computed once per page list.
227
163
  let collectionData;
228
164
  const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
229
165
  if (allPages) {
230
- if (isIndexPage) {
231
- // For index pages, show their own collection data
232
- collectionData = buildCollectionData(page, allPages);
233
- }
234
- else {
235
- // For child pages, show parent collection data
236
- collectionData = buildParentCollectionData(page, allPages);
166
+ const collectionPath = resolveCollectionPath(page, allPages);
167
+ const data = collectionPath !== undefined
168
+ ? getCollections(allPages).byPath.get(collectionPath)
169
+ : undefined;
170
+ if (data) {
171
+ collectionData = createCollectionView(data, reads);
237
172
  }
238
173
  }
239
174
  // Discover the appropriate layout using hierarchical layout.eta convention
@@ -258,28 +193,52 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
258
193
  if (conflictingKeys.length > 0) {
259
194
  log.warning(`Frontmatter in "${page.sourcePath}" contains reserved keys that will be overwritten: ${conflictingKeys.join(', ')}`);
260
195
  }
196
+ // The current page's navigation node counts as a navigation read
197
+ const pageContext = { ...page.frontMatter, ...pageOverrides };
198
+ Object.defineProperty(pageContext, 'navNode', {
199
+ enumerable: true,
200
+ configurable: true,
201
+ get() {
202
+ reads.nav = true;
203
+ return currentNavNode;
204
+ },
205
+ });
261
206
  // Create base context for partial rendering
262
207
  const baseContext = {
263
208
  site: config.site,
264
- page: {
265
- ...page.frontMatter,
266
- ...pageOverrides,
267
- },
209
+ page: pageContext,
268
210
  content: body,
269
- nav: navHelpers, // Replace navigation with nav helpers
211
+ nav: createNavView(navHelpers, reads), // Replace navigation with nav helpers
270
212
  collection: collectionData, // Add collection data
213
+ // Set on paginated listings and on pages generated from [param].md templates. Placed before
214
+ // the filters, like slugify, so that an existing filter with the same name keeps working.
215
+ pagination: page.pagination ? copyPaginationData(page.pagination) : undefined,
216
+ generated: page.generated ? copyGeneratedData(page.generated) : undefined,
217
+ slugify,
271
218
  // Add custom filters to context
272
219
  ...(config.eta?.filters || {}),
273
220
  generator: {
274
221
  version: getStatiVersion(),
275
222
  },
276
223
  // Stati utilities object with helper functions
277
- generateSEO: (tags) => generateSEO({ page, config, site: config.site }, tags),
224
+ generateSEO: (tags) => generateSEO({
225
+ page,
226
+ config,
227
+ site: config.site,
228
+ generatedImage: ogState.image,
229
+ deferGeneratedImage: ogState.pending,
230
+ }, tags),
278
231
  propValue,
232
+ // Image with dimensions, lazy loading and, with images.responsive, srcset variants
233
+ image: (src, options) => pageImages ? pageImages.renderHelperImage(src, options) : renderPlainImage(src, options),
279
234
  assets, // TypeScript bundle assets (available when typescript.enabled is true)
280
235
  // Set by callable partials invoked with props; always present so that
281
236
  // stati.props?.x works under the dev-mode context validation
282
237
  props: undefined,
238
+ // Set while the Open Graph template renders: { width, height }
239
+ og: undefined,
240
+ // The page's generated Open Graph image, once rendered: { url, path, width, height, alt }
241
+ ogImage: undefined,
283
242
  };
284
243
  // Render partials and store their content
285
244
  // Use multiple passes to allow partials to reference other partials
@@ -345,6 +304,27 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
345
304
  break;
346
305
  }
347
306
  }
307
+ // Render the Open Graph template with the layout's context, minus the page body, so the
308
+ // generated image is known before the layout renders its SEO tags
309
+ if (pageImages && ogState.pending) {
310
+ // Copy the page's property descriptors so that navNode stays a getter: spreading would
311
+ // read it and count every page with a card as reading the navigation
312
+ const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(baseContext.page));
313
+ ogPage.content = '';
314
+ const ogBaseContext = {
315
+ ...baseContext,
316
+ content: '',
317
+ page: ogPage,
318
+ og: pageImages.ogSize(),
319
+ };
320
+ const ogContext = createValidatingContextProxy({
321
+ ...ogBaseContext,
322
+ partials: createValidatingPartialsProxy(wrapPartialsAsCallable(eta, renderedPartials, partialPaths, ogBaseContext)),
323
+ });
324
+ ogState.image = (await pageImages.renderOg(ogContext, eta)) ?? undefined;
325
+ ogState.pending = false;
326
+ baseContext.ogImage = ogState.image;
327
+ }
348
328
  // Wrap final rendered partials as callable before passing to layout context
349
329
  const callablePartials = wrapPartialsAsCallable(eta, renderedPartials, partialPaths, baseContext);
350
330
  const context = createValidatingContextProxy({
@@ -354,10 +334,20 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
354
334
  try {
355
335
  if (!layoutPath) {
356
336
  log.warning('No layout template found, using fallback');
357
- return { html: createFallbackHtml(page, body), templatesLoaded };
337
+ return {
338
+ html: createFallbackHtml(page, body),
339
+ templatesLoaded,
340
+ ogImage: ogState.image,
341
+ reads,
342
+ };
358
343
  }
359
344
  const html = await eta.renderAsync(layoutPath, context);
360
- return { html, templatesLoaded };
345
+ return {
346
+ html: resolveGeneratedImagePlaceholders(html, ogState.image),
347
+ templatesLoaded,
348
+ ogImage: ogState.image,
349
+ reads,
350
+ };
361
351
  }
362
352
  catch (error) {
363
353
  log.error(`Error rendering layout ${layoutPath || 'unknown'}: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,7 +356,12 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
366
356
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
367
357
  throw templateError;
368
358
  }
369
- return { html: createFallbackHtml(page, body), templatesLoaded };
359
+ return {
360
+ html: createFallbackHtml(page, body),
361
+ templatesLoaded,
362
+ ogImage: ogState.image,
363
+ reads,
364
+ };
370
365
  }
371
366
  }
372
367
  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
  */
@@ -24,6 +24,14 @@ export declare function remove(path: string): Promise<void>;
24
24
  * Gets file stats with consistent error handling.
25
25
  */
26
26
  export declare function stat(filePath: string): Promise<fse.Stats>;
27
+ /**
28
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
29
+ */
30
+ export declare function lstat(filePath: string): Promise<fse.Stats>;
31
+ /**
32
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
33
+ */
34
+ export declare function realpath(filePath: string): Promise<string>;
27
35
  /**
28
36
  * Reads directory contents with consistent error handling.
29
37
  */
@@ -34,4 +42,24 @@ export declare function readdir<T extends boolean = false>(dirPath: string, opti
34
42
  * Copies a file with consistent error handling.
35
43
  */
36
44
  export declare function copyFile(src: string, dest: string): Promise<void>;
45
+ /**
46
+ * Reads a file as raw bytes. Returns null if the file does not exist.
47
+ */
48
+ export declare function readFileBuffer(filePath: string): Promise<Buffer | null>;
49
+ /**
50
+ * Reads a file as raw bytes synchronously. Returns null if the file does not exist.
51
+ * For code that must stay synchronous, such as template helpers.
52
+ */
53
+ export declare function readFileBufferSync(filePath: string): Buffer | null;
54
+ /**
55
+ * Gets file stats synchronously. Returns null if the path does not exist.
56
+ */
57
+ export declare function statSyncOrNull(filePath: string): Stats | null;
58
+ /**
59
+ * Lists every file under a directory, recursively.
60
+ *
61
+ * @param dirPath - Directory to list
62
+ * @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
63
+ */
64
+ export declare function listFilesRecursiveSync(dirPath: string): string[];
37
65
  //# 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;AAqDvD;;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,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAEhE;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhE;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,5 +1,7 @@
1
1
  import fse from 'fs-extra';
2
- const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
2
+ import { readFileSync, statSync, readdirSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, lstat: fseLstat, realpath: fseRealpath, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
3
5
  /**
4
6
  * Wraps fs-extra operations with consistent error handling.
5
7
  */
@@ -63,6 +65,18 @@ export async function remove(path) {
63
65
  export async function stat(filePath) {
64
66
  return wrapFsOperation('get stats for', fseStat(filePath), filePath);
65
67
  }
68
+ /**
69
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
70
+ */
71
+ export async function lstat(filePath) {
72
+ return wrapFsOperation('get stats for', fseLstat(filePath), filePath);
73
+ }
74
+ /**
75
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
76
+ */
77
+ export async function realpath(filePath) {
78
+ return wrapFsOperation('resolve', fseRealpath(filePath), filePath);
79
+ }
66
80
  /**
67
81
  * Reads directory contents with consistent error handling.
68
82
  */
@@ -86,3 +100,62 @@ export async function readdir(dirPath, options) {
86
100
  export async function copyFile(src, dest) {
87
101
  return wrapFsOperation('copy', fseCopyFile(src, dest), `${src} to ${dest}`);
88
102
  }
103
+ /**
104
+ * Reads a file as raw bytes. Returns null if the file does not exist.
105
+ */
106
+ export async function readFileBuffer(filePath) {
107
+ return wrapFsOperationNullable('read file', fseReadFile(filePath), filePath);
108
+ }
109
+ /**
110
+ * Reads a file as raw bytes synchronously. Returns null if the file does not exist.
111
+ * For code that must stay synchronous, such as template helpers.
112
+ */
113
+ export function readFileBufferSync(filePath) {
114
+ try {
115
+ return readFileSync(filePath);
116
+ }
117
+ catch (error) {
118
+ const nodeError = error;
119
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
120
+ return null;
121
+ }
122
+ throw new Error(`Failed to read file ${filePath}: ${nodeError.message}`, { cause: error });
123
+ }
124
+ }
125
+ /**
126
+ * Gets file stats synchronously. Returns null if the path does not exist.
127
+ */
128
+ export function statSyncOrNull(filePath) {
129
+ try {
130
+ return statSync(filePath);
131
+ }
132
+ catch (error) {
133
+ const nodeError = error;
134
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
135
+ return null;
136
+ }
137
+ throw new Error(`Failed to get stats for ${filePath}: ${nodeError.message}`, { cause: error });
138
+ }
139
+ }
140
+ /**
141
+ * Lists every file under a directory, recursively.
142
+ *
143
+ * @param dirPath - Directory to list
144
+ * @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
145
+ */
146
+ export function listFilesRecursiveSync(dirPath) {
147
+ let entries;
148
+ try {
149
+ entries = readdirSync(dirPath, { withFileTypes: true, recursive: true });
150
+ }
151
+ catch (error) {
152
+ const nodeError = error;
153
+ if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
154
+ return [];
155
+ }
156
+ throw new Error(`Failed to read directory ${dirPath}: ${nodeError.message}`, { cause: error });
157
+ }
158
+ return entries
159
+ .filter((entry) => entry.isFile())
160
+ .map((entry) => relative(dirPath, join(entry.parentPath, entry.name)).replace(/\\/g, '/'));
161
+ }
@@ -2,9 +2,9 @@
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';
6
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
7
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
5
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
7
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
9
9
  export { slugify } from './slugify.utils.js';
10
10
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.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,11 +3,11 @@
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, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
7
7
  // Path resolution utilities
8
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
8
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
9
9
  // Template discovery utilities
10
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
10
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
11
11
  // Template utilities
12
12
  export { propValue } from './template.utils.js';
13
13
  // Slugify utilities
@@ -131,4 +131,19 @@ export declare function normalizePathForComparison(filePath: string, basePath?:
131
131
  * ```
132
132
  */
133
133
  export declare function isPathWithinDirectory(baseDir: string, targetPath: string): boolean;
134
+ /**
135
+ * The file a page URL is written to, relative to the output directory. The build joins it to
136
+ * the output directory, which also normalizes separators and `.` segments.
137
+ *
138
+ * @param url - The page URL
139
+ * @returns The output file
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * getPageOutputFile('/'); // 'index.html'
144
+ * getPageOutputFile('/blog/'); // '/blog/index.html'
145
+ * getPageOutputFile('/blog/post'); // '/blog/post.html'
146
+ * ```
147
+ */
148
+ export declare function getPageOutputFile(url: string): string;
134
149
  //# sourceMappingURL=paths.utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF"}
1
+ {"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKrD"}