@krantho/core 0.0.0-stage → 1.0.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cache-headers.d.ts +39 -0
  3. package/dist/cache-headers.d.ts.map +1 -0
  4. package/dist/cache-headers.js +57 -0
  5. package/dist/cache-headers.js.map +1 -0
  6. package/dist/cache-tags.d.ts +88 -0
  7. package/dist/cache-tags.d.ts.map +1 -0
  8. package/dist/cache-tags.js +182 -0
  9. package/dist/cache-tags.js.map +1 -0
  10. package/dist/canonical-json.d.ts +7 -0
  11. package/dist/canonical-json.d.ts.map +1 -0
  12. package/dist/canonical-json.js +19 -0
  13. package/dist/canonical-json.js.map +1 -0
  14. package/dist/composition/index.d.ts +5 -0
  15. package/dist/composition/index.d.ts.map +1 -0
  16. package/dist/composition/index.js +3 -0
  17. package/dist/composition/index.js.map +1 -0
  18. package/dist/composition/params.d.ts +12 -0
  19. package/dist/composition/params.d.ts.map +1 -0
  20. package/dist/composition/params.js +23 -0
  21. package/dist/composition/params.js.map +1 -0
  22. package/dist/composition/resolve.d.ts +32 -0
  23. package/dist/composition/resolve.d.ts.map +1 -0
  24. package/dist/composition/resolve.js +61 -0
  25. package/dist/composition/resolve.js.map +1 -0
  26. package/dist/composition/types.d.ts +33 -0
  27. package/dist/composition/types.d.ts.map +1 -0
  28. package/dist/composition/types.js +5 -0
  29. package/dist/composition/types.js.map +1 -0
  30. package/dist/content-change.d.ts +32 -0
  31. package/dist/content-change.d.ts.map +1 -0
  32. package/dist/content-change.js +2 -0
  33. package/dist/content-change.js.map +1 -0
  34. package/dist/content-contract.d.ts +59 -0
  35. package/dist/content-contract.d.ts.map +1 -0
  36. package/dist/content-contract.js +10 -0
  37. package/dist/content-contract.js.map +1 -0
  38. package/dist/content-engine.d.ts +90 -0
  39. package/dist/content-engine.d.ts.map +1 -0
  40. package/dist/content-engine.js +287 -0
  41. package/dist/content-engine.js.map +1 -0
  42. package/dist/content-resolver.d.ts +110 -0
  43. package/dist/content-resolver.d.ts.map +1 -0
  44. package/dist/content-resolver.js +132 -0
  45. package/dist/content-resolver.js.map +1 -0
  46. package/dist/frontmatter-utils.d.ts +22 -0
  47. package/dist/frontmatter-utils.d.ts.map +1 -0
  48. package/dist/frontmatter-utils.js +50 -0
  49. package/dist/frontmatter-utils.js.map +1 -0
  50. package/dist/index.d.ts +28 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +31 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/loaders/client-fetch-loader.d.ts +13 -0
  55. package/dist/loaders/client-fetch-loader.d.ts.map +1 -0
  56. package/dist/loaders/client-fetch-loader.js +49 -0
  57. package/dist/loaders/client-fetch-loader.js.map +1 -0
  58. package/dist/loaders/seo-loader.d.ts +3 -0
  59. package/dist/loaders/seo-loader.d.ts.map +1 -0
  60. package/dist/loaders/seo-loader.js +31 -0
  61. package/dist/loaders/seo-loader.js.map +1 -0
  62. package/dist/marks.d.ts +35 -0
  63. package/dist/marks.d.ts.map +1 -0
  64. package/dist/marks.js +67 -0
  65. package/dist/marks.js.map +1 -0
  66. package/dist/processors/at-notation-processor.d.ts +7 -0
  67. package/dist/processors/at-notation-processor.d.ts.map +1 -0
  68. package/dist/processors/at-notation-processor.js +31 -0
  69. package/dist/processors/at-notation-processor.js.map +1 -0
  70. package/dist/sections.d.ts +39 -0
  71. package/dist/sections.d.ts.map +1 -0
  72. package/dist/sections.js +74 -0
  73. package/dist/sections.js.map +1 -0
  74. package/dist/semantic-style-utils.d.ts +3 -0
  75. package/dist/semantic-style-utils.d.ts.map +1 -0
  76. package/dist/semantic-style-utils.js +17 -0
  77. package/dist/semantic-style-utils.js.map +1 -0
  78. package/dist/shared-types.d.ts +16 -0
  79. package/dist/shared-types.d.ts.map +1 -0
  80. package/dist/shared-types.js +9 -0
  81. package/dist/shared-types.js.map +1 -0
  82. package/dist/site-path.d.ts +9 -0
  83. package/dist/site-path.d.ts.map +1 -0
  84. package/dist/site-path.js +24 -0
  85. package/dist/site-path.js.map +1 -0
  86. package/dist/timestamps.d.ts +19 -0
  87. package/dist/timestamps.d.ts.map +1 -0
  88. package/dist/timestamps.js +52 -0
  89. package/dist/timestamps.js.map +1 -0
  90. package/dist/type-guards.d.ts +10 -0
  91. package/dist/type-guards.d.ts.map +1 -0
  92. package/dist/type-guards.js +37 -0
  93. package/dist/type-guards.js.map +1 -0
  94. package/dist/types.d.ts +205 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +48 -0
  97. package/dist/types.js.map +1 -0
  98. package/package.json +54 -3
  99. package/README.md +0 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Descoped
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Response headers for a page a CDN caches, and for an editor's preview.
3
+ *
4
+ * A cached page is fresh at the CDN for `s-maxage` and never in the browser
5
+ * (`max-age=0`), so a purge reaches every visitor. It carries the tags of every read that
6
+ * rendered it, within the strictest per-response tag limit across CDNs.
7
+ */
8
+ /** The CDN in front of the site */
9
+ export type CdnProvider = 'vercel' | 'cloudfront' | 'gcp';
10
+ /** What `cdnCacheHeaders` needs to cache a page */
11
+ export interface CdnCacheHeaderOptions {
12
+ /** Tags of every read that rendered the page */
13
+ tags: readonly string[];
14
+ provider: CdnProvider;
15
+ /** How long the CDN serves the page as fresh */
16
+ sMaxAgeSec: number;
17
+ /** How long after that the CDN serves it while it re-renders */
18
+ staleWhileRevalidateSec: number;
19
+ /** The response header the CloudFront distribution reads tags from */
20
+ tagHeader: string;
21
+ }
22
+ /**
23
+ * Fit a page's tags into a CDN's per-response limit. Duplicates are dropped;
24
+ * beyond the budget, document tags give way to their collection's tag (a
25
+ * change to any document in it then refreshes the page), then the rest is cut.
26
+ * `k:site` is always kept, so a site-wide change still reaches the page.
27
+ */
28
+ export declare function pageTagBudget(tags: readonly string[], max?: number): string[];
29
+ /**
30
+ * The headers that cache a page at the CDN, tagged for purging. Vercel and
31
+ * Cloud CDN read the CDN lifetime from their own header, beside a
32
+ * `Cache-Control` that keeps the page out of the browser. CloudFront reads
33
+ * lifetimes only from `Cache-Control`, so there the lifetime is `s-maxage` in
34
+ * `Cache-Control` itself — browsers ignore it and revalidate every visit.
35
+ */
36
+ export declare function cdnCacheHeaders(options: CdnCacheHeaderOptions): Record<string, string>;
37
+ /** The headers of an editor's preview: never stored, never indexed */
38
+ export declare function previewHeaders(): Record<string, string>;
39
+ //# sourceMappingURL=cache-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-headers.d.ts","sourceRoot":"","sources":["../src/cache-headers.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,mCAAmC;AACnC,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,KAAK,CAAA;AAEzD,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,QAAQ,EAAE,WAAW,CAAA;IACrB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,uBAAuB,EAAE,MAAM,CAAA;IAC/B,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAA;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,SAAqB,GAAG,MAAM,EAAE,CAOzF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYtF;AAED,sEAAsE;AACtE,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEvD"}
@@ -0,0 +1,57 @@
1
+ import { collectionTagOf, SITE_TAG } from './cache-tags.js';
2
+ const DEFAULT_TAG_BUDGET = 50;
3
+ /**
4
+ * Fit a page's tags into a CDN's per-response limit. Duplicates are dropped;
5
+ * beyond the budget, document tags give way to their collection's tag (a
6
+ * change to any document in it then refreshes the page), then the rest is cut.
7
+ * `k:site` is always kept, so a site-wide change still reaches the page.
8
+ */
9
+ export function pageTagBudget(tags, max = DEFAULT_TAG_BUDGET) {
10
+ const unique = [...new Set(tags)];
11
+ if (unique.length <= max)
12
+ return unique;
13
+ const collapsed = [...new Set(unique.map((tag) => collectionTagOf(tag) ?? tag))];
14
+ if (collapsed.length <= max)
15
+ return collapsed;
16
+ if (!collapsed.includes(SITE_TAG))
17
+ return collapsed.slice(0, max);
18
+ return [SITE_TAG, ...collapsed.filter((tag) => tag !== SITE_TAG).slice(0, max - 1)];
19
+ }
20
+ /**
21
+ * The headers that cache a page at the CDN, tagged for purging. Vercel and
22
+ * Cloud CDN read the CDN lifetime from their own header, beside a
23
+ * `Cache-Control` that keeps the page out of the browser. CloudFront reads
24
+ * lifetimes only from `Cache-Control`, so there the lifetime is `s-maxage` in
25
+ * `Cache-Control` itself — browsers ignore it and revalidate every visit.
26
+ */
27
+ export function cdnCacheHeaders(options) {
28
+ const lifetime = `s-maxage=${options.sMaxAgeSec}, stale-while-revalidate=${options.staleWhileRevalidateSec}`;
29
+ const headers = options.provider === 'cloudfront'
30
+ ? { 'Cache-Control': `public, max-age=0, ${lifetime}` }
31
+ : {
32
+ 'Cache-Control': 'public, max-age=0, must-revalidate',
33
+ [cdnControlHeader(options.provider)]: `public, ${lifetime}`,
34
+ };
35
+ const tags = pageTagBudget(options.tags);
36
+ if (tags.length > 0)
37
+ headers[tagHeaderFor(options)] = tags.join(',');
38
+ return headers;
39
+ }
40
+ /** The headers of an editor's preview: never stored, never indexed */
41
+ export function previewHeaders() {
42
+ return { 'Cache-Control': 'private, no-store', 'X-Robots-Tag': 'noindex' };
43
+ }
44
+ function cdnControlHeader(provider) {
45
+ return provider === 'vercel' ? 'Vercel-CDN-Cache-Control' : 'CDN-Cache-Control';
46
+ }
47
+ function tagHeaderFor(options) {
48
+ switch (options.provider) {
49
+ case 'vercel':
50
+ return 'Vercel-Cache-Tag';
51
+ case 'cloudfront':
52
+ return options.tagHeader;
53
+ case 'gcp':
54
+ return 'Cache-Tag';
55
+ }
56
+ }
57
+ //# sourceMappingURL=cache-headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-headers.js","sourceRoot":"","sources":["../src/cache-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AA0B3D,MAAM,kBAAkB,GAAG,EAAE,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB,EAAE,GAAG,GAAG,kBAAkB;IAC7E,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IACjC,IAAI,MAAM,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,MAAM,CAAA;IACvC,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAA;IAChF,IAAI,SAAS,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,SAAS,CAAA;IAC7C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IACjE,OAAO,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;AACrF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,OAA8B;IAC5D,MAAM,QAAQ,GAAG,YAAY,OAAO,CAAC,UAAU,4BAA4B,OAAO,CAAC,uBAAuB,EAAE,CAAA;IAC5G,MAAM,OAAO,GACX,OAAO,CAAC,QAAQ,KAAK,YAAY;QAC/B,CAAC,CAAC,EAAE,eAAe,EAAE,sBAAsB,QAAQ,EAAE,EAAE;QACvD,CAAC,CAAC;YACE,eAAe,EAAE,oCAAoC;YACrD,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,QAAQ,EAAE;SAC5D,CAAA;IACP,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACpE,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,cAAc;IAC5B,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,cAAc,EAAE,SAAS,EAAE,CAAA;AAC5E,CAAC;AAED,SAAS,gBAAgB,CAAC,QAA4C;IACpE,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,mBAAmB,CAAA;AACjF,CAAC;AAED,SAAS,YAAY,CAAC,OAA8B;IAClD,QAAQ,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,KAAK,QAAQ;YACX,OAAO,kBAAkB,CAAA;QAC3B,KAAK,YAAY;YACf,OAAO,OAAO,CAAC,SAAS,CAAA;QAC1B,KAAK,KAAK;YACR,OAAO,WAAW,CAAA;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,88 @@
1
+ import type { ContentChange } from './content-change.js';
2
+ /**
3
+ * The cache tag vocabulary: what a page read registers, and what a change
4
+ * invalidates. A cached page carries the tags of every read that rendered it,
5
+ * so invalidating a tag refreshes exactly the pages that read it.
6
+ *
7
+ * Tags fit the strictest limits across cache targets: printable ASCII with no
8
+ * comma or space, lowercase (some CDNs match tags case-insensitively), at most
9
+ * 120 bytes (a longer tag is shortened with a hash).
10
+ */
11
+ /** Carried by every read: a site-wide change refreshes every page */
12
+ export declare const SITE_TAG = "k:site";
13
+ /** How an invalidated page is refreshed */
14
+ export type InvalidationMode =
15
+ /** The next request renders it fresh: for the document an editor just changed */
16
+ 'expire'
17
+ /** The next request is served the old page while it re-renders: for broad changes */
18
+ | 'stale';
19
+ /** Tags to invalidate, and how */
20
+ export interface CacheInvalidation {
21
+ tags: readonly string[];
22
+ mode: InvalidationMode;
23
+ }
24
+ /** One document, in every locale */
25
+ export declare function documentTag(collection: string, slug: string): string;
26
+ /** A collection's membership: its lists, and every page that lists it */
27
+ export declare function collectionTag(collection: string): string;
28
+ /** One SEO configuration (`site`, `collection:<name>`, `route:<pattern>`); every configuration without one */
29
+ export declare function seoTag(configId?: string): string;
30
+ /** The tags a document read registers */
31
+ export declare function documentReadTags(collection: string, slug: string): string[];
32
+ /** The tags a collection list read registers */
33
+ export declare function listReadTags(collection: string): string[];
34
+ /** The tags an SEO configuration read registers */
35
+ export declare function seoReadTags(configId: string): string[];
36
+ /**
37
+ * The collection tag a document tag belongs to, or null for any other tag (or a
38
+ * document tag whose collection was cut by shortening)
39
+ */
40
+ export declare function collectionTagOf(documentTagValue: string): string | null;
41
+ /**
42
+ * The tags a page's JSON-LD reads: the document, which holds its own SEO in its
43
+ * versioned metadata, and the site, collection and URL-pattern configurations
44
+ * it is composed from
45
+ */
46
+ export declare function jsonLdTags(collection: string, slug: string): string[];
47
+ /**
48
+ * Where a collection's documents render as pages: a pattern holding `:slug`
49
+ * (`/deploy/:slug`), a fixed path every document of the collection renders at
50
+ * (a singleton such as a landing page, `/`), or `false` for a collection whose
51
+ * documents have no page of their own.
52
+ */
53
+ export type CollectionPath = string | false;
54
+ /** What `documentPath()` reads of a collection: its name, and the `path` it declares */
55
+ export interface PageCollection {
56
+ readonly name: string;
57
+ readonly path?: CollectionPath | undefined;
58
+ }
59
+ /**
60
+ * A collection by name from a site's collections — or, for a name the site does
61
+ * not define, a collection with no declared `path`, which the default rule places.
62
+ */
63
+ export declare function pageCollection(collections: Readonly<Record<string, PageCollection>>, name: string): PageCollection;
64
+ /**
65
+ * Whether a collection's documents render as pages. Without a declared `path`,
66
+ * every collection does except `globals`.
67
+ */
68
+ export declare function rendersPages(collection: PageCollection): boolean;
69
+ /**
70
+ * The URL path a document is rendered at, or null when its collection has no
71
+ * pages. A collection's `path` decides: `:slug` is replaced by the slug, and the
72
+ * slug `index` renders at the pattern without its `/:slug` segment
73
+ * (`/docs/:slug` → `/docs`); a path without `:slug` is where every document of
74
+ * the collection renders. Without a `path`, `pages` render at the site root
75
+ * (`home` is `/`), `globals` have no page, and other collections render under
76
+ * their name.
77
+ */
78
+ export declare function documentPath(collection: PageCollection, slug: string): string | null;
79
+ /**
80
+ * Where a collection's pages live, for labelling the collection: null when it
81
+ * has no pages; a declared `path` without its `/:slug` segment (an empty
82
+ * remainder is `/`); without a `path`, `/` for `pages` and `/<name>` for any
83
+ * other collection.
84
+ */
85
+ export declare function collectionBasePath(collection: PageCollection): string | null;
86
+ /** What a change invalidates. A draft save and a media change leave rendered pages as they are. */
87
+ export declare function invalidationsFor(change: ContentChange): CacheInvalidation[];
88
+ //# sourceMappingURL=cache-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-tags.d.ts","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAExD;;;;;;;;GAQG;AAEH,qEAAqE;AACrE,eAAO,MAAM,QAAQ,WAAW,CAAA;AAEhC,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB;AAC1B,iFAAiF;AAC/E,QAAQ;AACV,qFAAqF;GACnF,OAAO,CAAA;AAEX,kCAAkC;AAClC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAKD,oCAAoC;AACpC,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,8GAA8G;AAC9G,wBAAgB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3E;AAED,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAEzD;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAUtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMvE;AAKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CASrE;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAA;AAE3C,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,GAAG,SAAS,CAAA;CAC3C;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,EACrD,IAAI,EAAE,MAAM,GACX,cAAc,CAEhB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,cAAc,GAAG,OAAO,CAGhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAK5E;AASD,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,EAAE,CAyB3E"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The cache tag vocabulary: what a page read registers, and what a change
3
+ * invalidates. A cached page carries the tags of every read that rendered it,
4
+ * so invalidating a tag refreshes exactly the pages that read it.
5
+ *
6
+ * Tags fit the strictest limits across cache targets: printable ASCII with no
7
+ * comma or space, lowercase (some CDNs match tags case-insensitively), at most
8
+ * 120 bytes (a longer tag is shortened with a hash).
9
+ */
10
+ /** Carried by every read: a site-wide change refreshes every page */
11
+ export const SITE_TAG = 'k:site';
12
+ const MAX_TAG_LENGTH = 120;
13
+ const DOCUMENT_TAG_PREFIX = 'k:d:';
14
+ /** One document, in every locale */
15
+ export function documentTag(collection, slug) {
16
+ return tag(`${DOCUMENT_TAG_PREFIX}${part(collection)}:${part(slug)}`);
17
+ }
18
+ /** A collection's membership: its lists, and every page that lists it */
19
+ export function collectionTag(collection) {
20
+ return tag(`k:c:${part(collection)}`);
21
+ }
22
+ /** One SEO configuration (`site`, `collection:<name>`, `route:<pattern>`); every configuration without one */
23
+ export function seoTag(configId) {
24
+ return configId === undefined ? 'k:seo' : tag(`k:seo:${part(configId)}`);
25
+ }
26
+ /** The tags a document read registers */
27
+ export function documentReadTags(collection, slug) {
28
+ return [SITE_TAG, documentTag(collection, slug)];
29
+ }
30
+ /** The tags a collection list read registers */
31
+ export function listReadTags(collection) {
32
+ return [SITE_TAG, collectionTag(collection)];
33
+ }
34
+ /** The tags an SEO configuration read registers */
35
+ export function seoReadTags(configId) {
36
+ const tags = [SITE_TAG, seoTag(), seoTag(configId)];
37
+ if (configId.startsWith('doc:')) {
38
+ const identity = configId.slice(4);
39
+ const separator = identity.indexOf('/');
40
+ if (separator > 0 && separator < identity.length - 1) {
41
+ tags.push(documentTag(identity.slice(0, separator), identity.slice(separator + 1)));
42
+ }
43
+ }
44
+ return tags;
45
+ }
46
+ /**
47
+ * The collection tag a document tag belongs to, or null for any other tag (or a
48
+ * document tag whose collection was cut by shortening)
49
+ */
50
+ export function collectionTagOf(documentTagValue) {
51
+ if (!documentTagValue.startsWith(DOCUMENT_TAG_PREFIX))
52
+ return null;
53
+ const end = documentTagValue.indexOf(':', DOCUMENT_TAG_PREFIX.length);
54
+ if (end === -1)
55
+ return null;
56
+ // The segment is already `part()`-encoded: the same one `collectionTag` builds
57
+ return tag(`k:c:${documentTagValue.slice(DOCUMENT_TAG_PREFIX.length, end)}`);
58
+ }
59
+ /** The tag every page's JSON-LD carries for the URL-pattern configurations: which patterns match is known only when the page composes */
60
+ const ROUTE_CONFIGS_TAG = 'route';
61
+ /**
62
+ * The tags a page's JSON-LD reads: the document, which holds its own SEO in its
63
+ * versioned metadata, and the site, collection and URL-pattern configurations
64
+ * it is composed from
65
+ */
66
+ export function jsonLdTags(collection, slug) {
67
+ return [
68
+ SITE_TAG,
69
+ documentTag(collection, slug),
70
+ seoTag(),
71
+ seoTag('site'),
72
+ seoTag(`collection:${collection}`),
73
+ seoTag(ROUTE_CONFIGS_TAG),
74
+ ];
75
+ }
76
+ const SLUG_PARAM = ':slug';
77
+ /**
78
+ * A collection by name from a site's collections — or, for a name the site does
79
+ * not define, a collection with no declared `path`, which the default rule places.
80
+ */
81
+ export function pageCollection(collections, name) {
82
+ return collections[name] ?? { name };
83
+ }
84
+ /**
85
+ * Whether a collection's documents render as pages. Without a declared `path`,
86
+ * every collection does except `globals`.
87
+ */
88
+ export function rendersPages(collection) {
89
+ if (collection.path !== undefined)
90
+ return collection.path !== false;
91
+ return collection.name !== 'globals';
92
+ }
93
+ /**
94
+ * The URL path a document is rendered at, or null when its collection has no
95
+ * pages. A collection's `path` decides: `:slug` is replaced by the slug, and the
96
+ * slug `index` renders at the pattern without its `/:slug` segment
97
+ * (`/docs/:slug` → `/docs`); a path without `:slug` is where every document of
98
+ * the collection renders. Without a `path`, `pages` render at the site root
99
+ * (`home` is `/`), `globals` have no page, and other collections render under
100
+ * their name.
101
+ */
102
+ export function documentPath(collection, slug) {
103
+ if (!rendersPages(collection))
104
+ return null;
105
+ const { name, path } = collection;
106
+ if (typeof path === 'string')
107
+ return fillPath(path, slug);
108
+ if (name === 'pages')
109
+ return slug === 'home' ? '/' : `/${slug}`;
110
+ return `/${name}/${slug}`;
111
+ }
112
+ /**
113
+ * Where a collection's pages live, for labelling the collection: null when it
114
+ * has no pages; a declared `path` without its `/:slug` segment (an empty
115
+ * remainder is `/`); without a `path`, `/` for `pages` and `/<name>` for any
116
+ * other collection.
117
+ */
118
+ export function collectionBasePath(collection) {
119
+ if (!rendersPages(collection))
120
+ return null;
121
+ const { name, path } = collection;
122
+ if (typeof path === 'string')
123
+ return path.replace(`/${SLUG_PARAM}`, '') || '/';
124
+ return name === 'pages' ? '/' : `/${name}`;
125
+ }
126
+ function fillPath(pattern, slug) {
127
+ if (!pattern.includes(SLUG_PARAM))
128
+ return pattern;
129
+ const segment = `/${SLUG_PARAM}`;
130
+ if (slug === 'index' && pattern.includes(segment))
131
+ return pattern.replace(segment, '') || '/';
132
+ return pattern.replaceAll(SLUG_PARAM, slug);
133
+ }
134
+ /** What a change invalidates. A draft save and a media change leave rendered pages as they are. */
135
+ export function invalidationsFor(change) {
136
+ switch (change.kind) {
137
+ case 'document':
138
+ if (!change.live)
139
+ return [];
140
+ return [
141
+ // A global is on every page: refresh them as they are visited, not all on the next request
142
+ {
143
+ tags: [documentTag(change.collection, change.slug)],
144
+ mode: change.collection === 'globals' ? 'stale' : 'expire',
145
+ },
146
+ { tags: [collectionTag(change.collection)], mode: 'stale' },
147
+ ];
148
+ case 'seo':
149
+ if (change.configId === undefined)
150
+ return [{ tags: [seoTag()], mode: 'stale' }];
151
+ // A URL pattern can match any page: every page's JSON-LD refreshes as it is visited
152
+ if (change.configId.startsWith('route:')) {
153
+ return [{ tags: [seoTag(ROUTE_CONFIGS_TAG)], mode: 'stale' }];
154
+ }
155
+ return [{ tags: [seoTag(change.configId)], mode: 'stale' }];
156
+ case 'media':
157
+ // Pages embed a media URL, never the asset record; the file is served fresh
158
+ return [];
159
+ case 'site':
160
+ return [{ tags: [SITE_TAG], mode: 'stale' }];
161
+ }
162
+ }
163
+ /** A tag segment: lowercase, and percent-encoded outside unreserved ASCII. `Foo` and `foo` share a tag — at worst a page is refreshed needlessly. */
164
+ function part(value) {
165
+ return encodeURIComponent(value.toLowerCase()).toLowerCase();
166
+ }
167
+ function tag(value) {
168
+ if (value.length <= MAX_TAG_LENGTH)
169
+ return value;
170
+ const hash = fnv1a(value);
171
+ return `${value.slice(0, MAX_TAG_LENGTH - hash.length - 1)}~${hash}`;
172
+ }
173
+ /** A short, stable hash (FNV-1a, 32-bit) — no platform crypto, so core stays portable */
174
+ function fnv1a(value) {
175
+ let hash = 0x811c9dc5;
176
+ for (let i = 0; i < value.length; i++) {
177
+ hash ^= value.charCodeAt(i);
178
+ hash = Math.imul(hash, 0x01000193);
179
+ }
180
+ return (hash >>> 0).toString(16).padStart(8, '0');
181
+ }
182
+ //# sourceMappingURL=cache-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,qEAAqE;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAA;AAehC,MAAM,cAAc,GAAG,GAAG,CAAA;AAC1B,MAAM,mBAAmB,GAAG,MAAM,CAAA;AAElC,oCAAoC;AACpC,MAAM,UAAU,WAAW,CAAC,UAAkB,EAAE,IAAY;IAC1D,OAAO,GAAG,CAAC,GAAG,mBAAmB,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACvE,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,OAAO,GAAG,CAAC,OAAO,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;AACvC,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,MAAM,CAAC,QAAiB;IACtC,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC1E,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAY;IAC/D,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,YAAY,CAAC,UAAkB;IAC7C,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;AAC9C,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IACnD,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAClC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACvC,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACrF,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,gBAAwB;IACtD,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,mBAAmB,CAAC;QAAE,OAAO,IAAI,CAAA;IAClE,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAA;IACrE,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,OAAO,GAAG,CAAC,OAAO,gBAAgB,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,yIAAyI;AACzI,MAAM,iBAAiB,GAAG,OAAO,CAAA;AAEjC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,IAAY;IACzD,OAAO;QACL,QAAQ;QACR,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC;QAC7B,MAAM,EAAE;QACR,MAAM,CAAC,MAAM,CAAC;QACd,MAAM,CAAC,cAAc,UAAU,EAAE,CAAC;QAClC,MAAM,CAAC,iBAAiB,CAAC;KAC1B,CAAA;AACH,CAAC;AAgBD,MAAM,UAAU,GAAG,OAAO,CAAA;AAE1B;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,WAAqD,EACrD,IAAY;IAEZ,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAA;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,UAA0B;IACrD,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,KAAK,KAAK,CAAA;IACnE,OAAO,UAAU,CAAC,IAAI,KAAK,SAAS,CAAA;AACtC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,UAA0B,EAAE,IAAY;IACnE,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,UAAU,CAAA;IACjC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IACzD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAA;IAC/D,OAAO,IAAI,IAAI,IAAI,IAAI,EAAE,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAA0B;IAC3D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,UAAU,CAAA;IACjC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,UAAU,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;IAC9E,OAAO,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAA;AAC5C,CAAC;AAED,SAAS,QAAQ,CAAC,OAAe,EAAE,IAAY;IAC7C,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,OAAO,CAAA;IACjD,MAAM,OAAO,GAAG,IAAI,UAAU,EAAE,CAAA;IAChC,IAAI,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;IAC7F,OAAO,OAAO,CAAC,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;AAC7C,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,UAAU;YACb,IAAI,CAAC,MAAM,CAAC,IAAI;gBAAE,OAAO,EAAE,CAAA;YAC3B,OAAO;gBACL,2FAA2F;gBAC3F;oBACE,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;oBACnD,IAAI,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;iBAC3D;gBACD,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE;aAC5D,CAAA;QACH,KAAK,KAAK;YACR,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;YAC/E,oFAAoF;YACpF,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;YAC/D,CAAC;YACD,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAC7D,KAAK,OAAO;YACV,4EAA4E;YAC5E,OAAO,EAAE,CAAA;QACX,KAAK,MAAM;YACT,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IAChD,CAAC;AACH,CAAC;AAED,qJAAqJ;AACrJ,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AAC9D,CAAC;AAED,SAAS,GAAG,CAAC,KAAa;IACxB,IAAI,KAAK,CAAC,MAAM,IAAI,cAAc;QAAE,OAAO,KAAK,CAAA;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,CAAA;AACtE,CAAC;AAED,yFAAyF;AACzF,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,IAAI,GAAG,UAAU,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACnD,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * JSON with object keys sorted by code unit at every depth, so equal values
3
+ * serialize identically on every machine. Undefined object properties are
4
+ * left out, as JSON.stringify does; array order is part of the value.
5
+ */
6
+ export declare function canonicalJson(value: unknown): string;
7
+ //# sourceMappingURL=canonical-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-json.d.ts","sourceRoot":"","sources":["../src/canonical-json.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUpD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * JSON with object keys sorted by code unit at every depth, so equal values
3
+ * serialize identically on every machine. Undefined object properties are
4
+ * left out, as JSON.stringify does; array order is part of the value.
5
+ */
6
+ export function canonicalJson(value) {
7
+ if (value instanceof Date)
8
+ return JSON.stringify(value.toISOString());
9
+ if (Array.isArray(value))
10
+ return `[${value.map(canonicalJson).join(',')}]`;
11
+ if (value !== null && typeof value === 'object') {
12
+ const entries = Object.entries(value)
13
+ .filter(([, entry]) => entry !== undefined)
14
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
15
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
16
+ }
17
+ return JSON.stringify(value) ?? 'null';
18
+ }
19
+ //# sourceMappingURL=canonical-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-json.js","sourceRoot":"","sources":["../src/canonical-json.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IACrE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IAC1E,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;aAClC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;aAC1C,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACnD,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACzG,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAA;AACxC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { resolveParams } from './params.js';
2
+ export type { ResolveContext } from './resolve.js';
3
+ export { resolvePageContent } from './resolve.js';
4
+ export type { CollectionItem, CollectionResult, ResolvedPageContent, SectionItem, SectionsResult, SlugResult, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/composition/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,UAAU,GACX,MAAM,YAAY,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { resolveParams } from './params.js';
2
+ export { resolvePageContent } from './resolve.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/composition/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolve dynamic :param segments in a string using provided params.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * resolveParams('blog/:slug', { slug: 'my-post' }) // 'blog/my-post'
7
+ * resolveParams(':slug', { slug: 'hello' }) // 'hello'
8
+ * resolveParams('home', {}) // 'home' (no params)
9
+ * ```
10
+ */
11
+ export declare function resolveParams(template: string, params?: Record<string, string>): string;
12
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/composition/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAYvF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolve dynamic :param segments in a string using provided params.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * resolveParams('blog/:slug', { slug: 'my-post' }) // 'blog/my-post'
7
+ * resolveParams(':slug', { slug: 'hello' }) // 'hello'
8
+ * resolveParams('home', {}) // 'home' (no params)
9
+ * ```
10
+ */
11
+ export function resolveParams(template, params) {
12
+ if (!params || !template.includes(':')) {
13
+ return template;
14
+ }
15
+ return template.replace(/:(\w+)/g, (match, key) => {
16
+ const value = params[key];
17
+ if (value === undefined) {
18
+ throw new Error(`Missing param "${key}" for template "${template}"`);
19
+ }
20
+ return value;
21
+ });
22
+ }
23
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/composition/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,MAA+B;IAC7E,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAChD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAa,CAAC,CAAA;QACnC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,kBAAkB,GAAG,mBAAmB,QAAQ,GAAG,CAAC,CAAA;QACtE,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { ContentRef as DocumentRef, IPublishedContentLoader } from '../types.js';
2
+ import type { ResolvedPageContent } from './types.js';
3
+ interface SlugRef extends DocumentRef {
4
+ readonly type: 'slug';
5
+ }
6
+ interface SectionsRef extends DocumentRef {
7
+ readonly type: 'sections';
8
+ readonly names: readonly string[] | '*';
9
+ }
10
+ interface CollectionRef {
11
+ readonly type: 'collection';
12
+ readonly collection: string;
13
+ readonly locale?: string | undefined;
14
+ readonly options: {
15
+ sort?: string | undefined;
16
+ limit?: number | undefined;
17
+ offset?: number | undefined;
18
+ exclude?: string | undefined;
19
+ };
20
+ }
21
+ interface PageDefinition {
22
+ readonly route: string;
23
+ readonly content: Readonly<Record<string, SlugRef | SectionsRef | CollectionRef>>;
24
+ }
25
+ /** The server loader owns published reads, preview isolation and dependency tags. */
26
+ export interface ResolveContext {
27
+ loader: IPublishedContentLoader;
28
+ }
29
+ /** Explicit collection identity prevents collisions; visitors only receive published revisions. */
30
+ export declare function resolvePageContent(page: PageDefinition, context: ResolveContext, params?: Record<string, string>): Promise<ResolvedPageContent>;
31
+ export {};
32
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/composition/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,IAAI,WAAW,EACzB,uBAAuB,EAExB,MAAM,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAkB,mBAAmB,EAA8B,MAAM,YAAY,CAAA;AAEjG,UAAU,OAAQ,SAAQ,WAAW;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AACD,UAAU,WAAY,SAAQ,WAAW;IACvC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAA;CACxC;AACD,UAAU,aAAa;IACrB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE;QAChB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAC7B,CAAA;CACF;AACD,UAAU,cAAc;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,aAAa,CAAC,CAAC,CAAA;CAClF;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,uBAAuB,CAAA;CAChC;AAiBD,mGAAmG;AACnG,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,mBAAmB,CAAC,CAwB9B"}
@@ -0,0 +1,61 @@
1
+ import { renderContent } from '../content-engine.js';
2
+ import { resolveParams } from './params.js';
3
+ async function rendered(document) {
4
+ const { html, sections } = await renderContent(document.fields, document.body, {
5
+ atNotation: false,
6
+ });
7
+ return { fields: document.fields, metadata: document.metadata, html, sections };
8
+ }
9
+ function sectionsOf(content, names) {
10
+ return (names === '*' ? Object.keys(content.sections) : names).map((name) => ({
11
+ name,
12
+ title: name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
13
+ html: content.sections[name] ?? '',
14
+ }));
15
+ }
16
+ /** Explicit collection identity prevents collisions; visitors only receive published revisions. */
17
+ export async function resolvePageContent(page, context, params) {
18
+ const cache = new Map();
19
+ const load = (ref) => {
20
+ const resolved = { ...ref, slug: resolveParams(ref.slug, params) };
21
+ const key = JSON.stringify([resolved.collection, resolved.slug, resolved.locale]);
22
+ let promise = cache.get(key);
23
+ if (!promise) {
24
+ promise = context.loader
25
+ .getPublished(resolved)
26
+ .then((document) => (document ? rendered(document) : null));
27
+ cache.set(key, promise);
28
+ }
29
+ return promise;
30
+ };
31
+ const entries = await Promise.all(Object.entries(page.content).map(async ([key, ref]) => {
32
+ if (ref.type === 'collection')
33
+ return [key, await resolveCollection(ref, context.loader, params)];
34
+ const content = await load(ref);
35
+ if (!content)
36
+ return [key, null];
37
+ return [key, ref.type === 'sections' ? sectionsOf(content, ref.names) : content];
38
+ }));
39
+ return Object.fromEntries(entries);
40
+ }
41
+ async function resolveCollection(ref, loader, params) {
42
+ const exclude = ref.options.exclude ? resolveParams(ref.options.exclude, params) : undefined;
43
+ const documents = await loader.listPublished(ref.collection, ref.locale);
44
+ let items = await Promise.all(documents
45
+ .filter((document) => document.slug !== exclude)
46
+ .map(async (document) => ({
47
+ slug: document.slug,
48
+ ...(await rendered(document)),
49
+ excerpt: typeof document.fields['excerpt'] === 'string' ? document.fields['excerpt'] : undefined,
50
+ })));
51
+ if (ref.options.sort) {
52
+ const [field = 'slug', direction] = ref.options.sort.split(':');
53
+ items = items.sort((a, b) => {
54
+ const compare = String(a.fields[field] ?? a.slug).localeCompare(String(b.fields[field] ?? b.slug));
55
+ return direction === 'desc' ? -compare : compare;
56
+ });
57
+ }
58
+ const offset = ref.options.offset ?? 0;
59
+ return items.slice(offset, offset + (ref.options.limit ?? items.length));
60
+ }
61
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/composition/resolve.ts"],"names":[],"mappings":"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"}