@glw907/cairn-cms 0.24.0 → 0.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 (193) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +50 -37
  3. package/dist/auth/crypto.d.ts +0 -1
  4. package/dist/auth/store.d.ts +0 -1
  5. package/dist/auth/types.d.ts +0 -1
  6. package/dist/components/AdminLayout.svelte.d.ts +0 -1
  7. package/dist/components/ComponentForm.svelte.d.ts +0 -1
  8. package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
  9. package/dist/components/ConceptList.svelte.d.ts +0 -1
  10. package/dist/components/ConfirmPage.svelte.d.ts +0 -1
  11. package/dist/components/DeleteDialog.svelte.d.ts +0 -1
  12. package/dist/components/EditPage.svelte.d.ts +0 -1
  13. package/dist/components/EditorToolbar.svelte.d.ts +0 -1
  14. package/dist/components/IconPicker.svelte.d.ts +0 -1
  15. package/dist/components/LinkPicker.svelte.d.ts +0 -1
  16. package/dist/components/LoginPage.svelte.d.ts +0 -1
  17. package/dist/components/ManageEditors.svelte.d.ts +0 -1
  18. package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
  19. package/dist/components/NavTree.svelte.d.ts +0 -1
  20. package/dist/components/RenameDialog.svelte.d.ts +0 -1
  21. package/dist/components/index.d.ts +0 -1
  22. package/dist/components/link-completion.d.ts +0 -1
  23. package/dist/components/markdown-format.d.ts +0 -1
  24. package/dist/content/adapter.d.ts +0 -1
  25. package/dist/content/compose.d.ts +15 -5
  26. package/dist/content/compose.js +9 -5
  27. package/dist/content/concepts.d.ts +7 -1
  28. package/dist/content/concepts.js +49 -1
  29. package/dist/content/frontmatter.d.ts +0 -1
  30. package/dist/content/identity.d.ts +23 -0
  31. package/dist/content/identity.js +43 -0
  32. package/dist/content/ids.d.ts +0 -1
  33. package/dist/content/links.d.ts +0 -1
  34. package/dist/content/manifest.d.ts +23 -5
  35. package/dist/content/manifest.js +55 -32
  36. package/dist/content/permalink.d.ts +0 -1
  37. package/dist/content/schema.d.ts +0 -1
  38. package/dist/content/types.d.ts +0 -1
  39. package/dist/content/validate.d.ts +4 -2
  40. package/dist/content/validate.js +4 -1
  41. package/dist/delivery/CairnHead.svelte.d.ts +0 -1
  42. package/dist/delivery/content-index.d.ts +4 -1
  43. package/dist/delivery/content-index.js +8 -25
  44. package/dist/delivery/data.d.ts +23 -0
  45. package/dist/delivery/data.js +18 -0
  46. package/dist/delivery/excerpt.d.ts +0 -1
  47. package/dist/delivery/feeds.d.ts +0 -1
  48. package/dist/delivery/head.d.ts +0 -1
  49. package/dist/delivery/index.d.ts +1 -24
  50. package/dist/delivery/index.js +5 -20
  51. package/dist/delivery/json-ld.d.ts +0 -1
  52. package/dist/delivery/manifest.d.ts +0 -1
  53. package/dist/delivery/paginate.d.ts +0 -1
  54. package/dist/delivery/responses.d.ts +0 -1
  55. package/dist/delivery/robots.d.ts +0 -1
  56. package/dist/delivery/seo-fields.d.ts +0 -1
  57. package/dist/delivery/seo.d.ts +0 -1
  58. package/dist/delivery/site-descriptors.d.ts +0 -1
  59. package/dist/delivery/site-descriptors.js +5 -6
  60. package/dist/delivery/site-index.d.ts +0 -1
  61. package/dist/delivery/site-indexes.d.ts +0 -1
  62. package/dist/delivery/sitemap.d.ts +0 -1
  63. package/dist/email.d.ts +0 -1
  64. package/dist/env.d.ts +0 -1
  65. package/dist/github/credentials.d.ts +0 -1
  66. package/dist/github/repo.d.ts +0 -1
  67. package/dist/github/signing.d.ts +0 -1
  68. package/dist/github/types.d.ts +0 -1
  69. package/dist/index.d.ts +4 -30
  70. package/dist/index.js +2 -24
  71. package/dist/nav/site-config.d.ts +0 -1
  72. package/dist/render/component-grammar.d.ts +0 -1
  73. package/dist/render/component-insert.d.ts +0 -1
  74. package/dist/render/component-reference.d.ts +0 -1
  75. package/dist/render/component-validate.d.ts +0 -1
  76. package/dist/render/glyph.d.ts +0 -1
  77. package/dist/render/index.d.ts +0 -1
  78. package/dist/render/pipeline.d.ts +2 -3
  79. package/dist/render/pipeline.js +7 -2
  80. package/dist/render/registry.d.ts +0 -1
  81. package/dist/render/rehype-dispatch.d.ts +0 -1
  82. package/dist/render/remark-directives.d.ts +0 -1
  83. package/dist/render/resolve-links.d.ts +0 -1
  84. package/dist/render/sanitize-schema.d.ts +14 -1
  85. package/dist/render/sanitize-schema.js +96 -0
  86. package/dist/sveltekit/auth-routes.d.ts +0 -1
  87. package/dist/sveltekit/content-routes.d.ts +0 -1
  88. package/dist/sveltekit/editors-routes.d.ts +0 -1
  89. package/dist/sveltekit/guard.d.ts +0 -1
  90. package/dist/sveltekit/health.d.ts +0 -1
  91. package/dist/sveltekit/index.d.ts +1 -3
  92. package/dist/sveltekit/index.js +0 -1
  93. package/dist/sveltekit/nav-routes.d.ts +0 -1
  94. package/dist/sveltekit/public-routes.d.ts +0 -1
  95. package/dist/sveltekit/types.d.ts +0 -1
  96. package/dist/vite/bin.d.ts +2 -0
  97. package/dist/vite/bin.js +9 -0
  98. package/dist/vite/index.d.ts +32 -0
  99. package/dist/vite/index.js +178 -0
  100. package/package.json +22 -4
  101. package/src/lib/content/compose.ts +19 -10
  102. package/src/lib/content/concepts.ts +61 -1
  103. package/src/lib/content/identity.ts +60 -0
  104. package/src/lib/content/manifest.ts +69 -34
  105. package/src/lib/content/validate.ts +4 -1
  106. package/src/lib/delivery/content-index.ts +12 -27
  107. package/src/lib/delivery/data.ts +26 -0
  108. package/src/lib/delivery/index.ts +5 -28
  109. package/src/lib/delivery/site-descriptors.ts +5 -6
  110. package/src/lib/index.ts +4 -57
  111. package/src/lib/render/pipeline.ts +9 -3
  112. package/src/lib/render/sanitize-schema.ts +97 -0
  113. package/src/lib/sveltekit/index.ts +2 -8
  114. package/src/lib/vite/bin.ts +10 -0
  115. package/src/lib/vite/index.ts +213 -0
  116. package/dist/auth/crypto.d.ts.map +0 -1
  117. package/dist/auth/store.d.ts.map +0 -1
  118. package/dist/auth/types.d.ts.map +0 -1
  119. package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
  120. package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
  121. package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
  122. package/dist/components/ConceptList.svelte.d.ts.map +0 -1
  123. package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
  124. package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
  125. package/dist/components/EditPage.svelte.d.ts.map +0 -1
  126. package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
  127. package/dist/components/IconPicker.svelte.d.ts.map +0 -1
  128. package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
  129. package/dist/components/LoginPage.svelte.d.ts.map +0 -1
  130. package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
  131. package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
  132. package/dist/components/NavTree.svelte.d.ts.map +0 -1
  133. package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
  134. package/dist/components/index.d.ts.map +0 -1
  135. package/dist/components/link-completion.d.ts.map +0 -1
  136. package/dist/components/markdown-format.d.ts.map +0 -1
  137. package/dist/content/adapter.d.ts.map +0 -1
  138. package/dist/content/compose.d.ts.map +0 -1
  139. package/dist/content/concepts.d.ts.map +0 -1
  140. package/dist/content/frontmatter.d.ts.map +0 -1
  141. package/dist/content/ids.d.ts.map +0 -1
  142. package/dist/content/links.d.ts.map +0 -1
  143. package/dist/content/manifest.d.ts.map +0 -1
  144. package/dist/content/permalink.d.ts.map +0 -1
  145. package/dist/content/schema.d.ts.map +0 -1
  146. package/dist/content/types.d.ts.map +0 -1
  147. package/dist/content/validate.d.ts.map +0 -1
  148. package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
  149. package/dist/delivery/content-index.d.ts.map +0 -1
  150. package/dist/delivery/excerpt.d.ts.map +0 -1
  151. package/dist/delivery/feeds.d.ts.map +0 -1
  152. package/dist/delivery/head.d.ts.map +0 -1
  153. package/dist/delivery/index.d.ts.map +0 -1
  154. package/dist/delivery/json-ld.d.ts.map +0 -1
  155. package/dist/delivery/manifest.d.ts.map +0 -1
  156. package/dist/delivery/paginate.d.ts.map +0 -1
  157. package/dist/delivery/responses.d.ts.map +0 -1
  158. package/dist/delivery/robots.d.ts.map +0 -1
  159. package/dist/delivery/seo-fields.d.ts.map +0 -1
  160. package/dist/delivery/seo.d.ts.map +0 -1
  161. package/dist/delivery/site-descriptors.d.ts.map +0 -1
  162. package/dist/delivery/site-index.d.ts.map +0 -1
  163. package/dist/delivery/site-indexes.d.ts.map +0 -1
  164. package/dist/delivery/sitemap.d.ts.map +0 -1
  165. package/dist/email.d.ts.map +0 -1
  166. package/dist/env.d.ts.map +0 -1
  167. package/dist/github/credentials.d.ts.map +0 -1
  168. package/dist/github/repo.d.ts.map +0 -1
  169. package/dist/github/signing.d.ts.map +0 -1
  170. package/dist/github/types.d.ts.map +0 -1
  171. package/dist/index.d.ts.map +0 -1
  172. package/dist/nav/site-config.d.ts.map +0 -1
  173. package/dist/render/component-grammar.d.ts.map +0 -1
  174. package/dist/render/component-insert.d.ts.map +0 -1
  175. package/dist/render/component-reference.d.ts.map +0 -1
  176. package/dist/render/component-validate.d.ts.map +0 -1
  177. package/dist/render/glyph.d.ts.map +0 -1
  178. package/dist/render/index.d.ts.map +0 -1
  179. package/dist/render/pipeline.d.ts.map +0 -1
  180. package/dist/render/registry.d.ts.map +0 -1
  181. package/dist/render/rehype-dispatch.d.ts.map +0 -1
  182. package/dist/render/remark-directives.d.ts.map +0 -1
  183. package/dist/render/resolve-links.d.ts.map +0 -1
  184. package/dist/render/sanitize-schema.d.ts.map +0 -1
  185. package/dist/sveltekit/auth-routes.d.ts.map +0 -1
  186. package/dist/sveltekit/content-routes.d.ts.map +0 -1
  187. package/dist/sveltekit/editors-routes.d.ts.map +0 -1
  188. package/dist/sveltekit/guard.d.ts.map +0 -1
  189. package/dist/sveltekit/health.d.ts.map +0 -1
  190. package/dist/sveltekit/index.d.ts.map +0 -1
  191. package/dist/sveltekit/nav-routes.d.ts.map +0 -1
  192. package/dist/sveltekit/public-routes.d.ts.map +0 -1
  193. package/dist/sveltekit/types.d.ts.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,136 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are recorded here, most recent first.
4
+
5
+ ## 0.29.0
6
+
7
+ Consolidated the URL-identity model. A content entry's id, slug, date, and permalink are now derived in
8
+ one place (`entryIdentity`), so the content index and the manifest cannot drift on an entry's URL, and a
9
+ site's concept descriptors are resolved through one path shared by the admin runtime and the delivery
10
+ layer. No public surface changed.
11
+
12
+ The YAML URL policy is now validated at build. A permalink pattern must be root-relative and use only the
13
+ tokens `:slug`, `:year`, `:month`, and `:day`, a date token is valid only on a dated concept, a
14
+ `datePrefix` must be `year`, `month`, or `day`, and a policy keyed to an undeclared concept fails the
15
+ build.
16
+
17
+ Behavior note: a site whose `content:` URL policy was malformed and silently defaulted will now fail the
18
+ build with a named error. A valid policy is unaffected.
19
+
20
+ ## 0.28.0
21
+
22
+ ### Security
23
+ Closed the render attribute-sink residual by construction. A new post-dispatch guard runs last in
24
+ `createRenderer` and neutralizes the sinks a component `build()` could route a raw author attribute
25
+ value into, including the unsafe URL schemes `javascript:`, `data:`, and `vbscript:` in `href`,
26
+ `src`, `srcset`, `xlink:href`, `poster`, `formaction`, `action`, `object`'s `data`, and
27
+ `background`, the inline `on*` event handlers, and inline `style`, which is stripped wholesale. Safe
28
+ schemes, relative URLs, anchors, and the `cairn:` token are preserved. The guard is gated by the
29
+ existing `unsafeDisableSanitize` switch.
30
+
31
+ Behavior note: a site whose component `build()` emits a non-standard URL scheme, an `on*` handler,
32
+ or inline `style` will see that output neutralized. Route dynamic styling through a class or an
33
+ inert `data-*` attribute instead.
34
+
35
+ ## 0.27.0
36
+
37
+ ### Changed (breaking)
38
+ Narrowed the public export surface so each symbol has one canonical home. The `.` root and
39
+ `/sveltekit` no longer re-export another subpath's symbols, and the internal GitHub, signing, and
40
+ hast helpers left the public API. No symbol changed behavior; only where it exports from.
41
+
42
+ - Consumers must: import the delivery read helpers (`createContentIndex`, `createSiteIndexes`, the
43
+ feed, sitemap, robots, SEO, and pagination builders, `permalink`) from `@glw907/cairn-cms/delivery/data`
44
+ instead of the `.` root.
45
+ - Consumers must: import the public route loaders and the `*Response` helpers (`createPublicRoutes`,
46
+ `rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`) and the public route types
47
+ (`PublicRoutesDeps`, the public `ListData`, `TagData`, `TagIndexData`, `EntryData`) from
48
+ `@glw907/cairn-cms/delivery` instead of the `.` root or `/sveltekit`.
49
+ - Consumers must: stop importing the internal helpers that left the public API (`appJwt`,
50
+ `installationToken`, `signingSelfTest`, `appCredentials`, `treeUrl`, `contentsUrl`, `readRaw`,
51
+ `fileSha`, `listMarkdown`, `markdownFilesIn`, `commitFile`, `isElement`, `strProp`, `markFirstList`);
52
+ the engine wires GitHub token minting and the render pipeline internally, so no consumer needs them.
53
+
54
+ ## 0.26.0
55
+
56
+ ### Added
57
+ - A `cairnManifest()` Vite plugin (`@glw907/cairn-cms/vite`) verifies the committed content manifest on
58
+ every build and fails the build with a diff naming what drifted. The check runs outside the prerender
59
+ lifecycle, so `handleHttpError` cannot mask it. Consumers must: add `cairnManifest({ configModule,
60
+ content, manifestPath })` to the Vite config.
61
+ - A `cairn-manifest` bin regenerates the committed manifest from a Vite context. Consumers must: set the
62
+ regenerate script to `"cairn:manifest": "cairn-manifest"` and delete the hand-written
63
+ `scripts/build-manifest.mjs`.
64
+ - A node-safe `@glw907/cairn-cms/delivery/data` entry exposes the pure delivery projections with no
65
+ `@sveltejs/kit` in the graph. Consumers must: move any plain-Node import of a delivery data helper
66
+ (such as `buildSiteManifest`) from `@glw907/cairn-cms/delivery` to `@glw907/cairn-cms/delivery/data`.
67
+
68
+ ### Changed
69
+ - `verifyManifest` now throws an error that names the added, removed, and changed entries. Consumers
70
+ must: nothing. The message is strictly more informative.
71
+
72
+ ## 0.25.0
73
+
74
+ ### Changed (breaking)
75
+ - `composeRuntime` now takes a single object, `composeRuntime({ adapter, siteConfig, extensions? })`,
76
+ and derives the per-concept URL policy from `siteConfig`. The loose third `urlPolicy` argument is
77
+ gone, and a missing `siteConfig` throws. Consumers must: pass the parsed site config to every
78
+ `composeRuntime` call and drop any hand-passed URL policy.
79
+
80
+ ### Changed
81
+ - `createRenderer()` now defaults its registry to the empty registry, so a plain-prose site calls
82
+ `createRenderer()` with no argument. Consumers must: nothing; passing a built registry is unchanged.
83
+
84
+ ### Docs
85
+ - A render sanitize-floor reference (`docs/render-sanitize-floor.md`) states what the floor keeps,
86
+ strips, and rewrites, including the `target="_blank"` rel policy.
87
+ - An upgrade guide (`docs/upgrading.md`) collects the `0.x` renames with a consumer action each.
88
+
89
+ ## 0.24.0
90
+
91
+ ### Added
92
+ - `headRow(title, icon?)` builds the icon-plus-heading component head, exported beside `cardShell` and
93
+ `iconSpan`.
94
+ - A `createRenderer` `anchorRel` option sets the `rel` value forced on `target="_blank"` anchors
95
+ (default `'noopener noreferrer'`), or disables the injection when set to `false`.
96
+
97
+ ### Changed
98
+ - A component's `defaultIconByRole` default now reaches the build through the declared `type: 'icon'`
99
+ attribute (`ctx.attributes`), so a role default no longer needs a hardcoded fallback in the build. A
100
+ component using `defaultIconByRole` must declare a `type: 'icon'` attribute.
101
+ - The engine drops an unclaimed directive `[label]` when a component has no `title` slot, so a stray
102
+ `[]` no longer renders an empty paragraph.
103
+
104
+ ### Removed
105
+ - The internal `data-icon` marker, which no build read. The resolved icon now travels on the declared
106
+ attribute path.
107
+
108
+ ## 0.23.0
109
+
110
+ ### Changed (breaking)
111
+ - A `date` field now validates a real `YYYY-MM-DD` calendar date. A site adopting this version whose
112
+ committed content holds a malformed or impossible date will see it fail validation, which is the loud
113
+ failure this restores.
114
+ - A `tags` field now enforces its declared `options` as a closed vocabulary. A committed value outside
115
+ the list fails validation. Use a `freetags` field for free-form tags.
116
+ - `normalizeConcepts` now throws when a `summaryFields` key names no declared field, so a typo fails at
117
+ config load instead of silently producing an empty list card.
118
+
119
+ ### Changed
120
+ - `AttributeField.options` is now `readonly string[]`, so a site can share one frozen `as const`
121
+ vocabulary across components. Read-only by use, so no call site changes.
122
+
123
+ ## 0.22.0
124
+
125
+ ### Added
126
+ - `ContentSummary.concept` and `EntryData.concept`: the read model carries its resolved concept id, so a
127
+ list or page branches per concept without re-deriving it from `entry.date`.
128
+ - A `summaryFields` knob on a concept config surfaces named frontmatter keys on `ContentSummary.fields`,
129
+ so a list card reads an authored field with no per-entry detail read.
130
+ - The package root re-exports the delivery route loaders (`createPublicRoutes`) and the response helpers
131
+ (`rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`).
132
+
133
+ ### Changed (breaking)
134
+ - `CairnHead` moved off the `@glw907/cairn-cms/delivery` barrel to its own `@glw907/cairn-cms/delivery/head`
135
+ entry, so a node-environment data import from `/delivery` stays component-free. Update the import:
136
+ `import { CairnHead } from '@glw907/cairn-cms/delivery/head'`.
package/README.md CHANGED
@@ -1,32 +1,33 @@
1
1
  # cairn-cms
2
2
 
3
3
  An embedded, **magic-link**, GitHub-committing CMS for SvelteKit + Cloudflare sites.
4
- Non-technical authors log in by email (no GitHub account, no password), edit **raw
5
- markdown** in a [Carta](https://github.com/BearToCode/carta) editor, and save. Each save
6
- commits to `main` via a **GitHub App** (committer = `cairn-cms[bot]`, author = the editor)
7
- and auto-deploys.
4
+ Non-technical authors log in by email (no GitHub account, no password), edit **raw markdown**
5
+ in a client-only CodeMirror editor with a live preview, and save. Each save commits to `main`
6
+ via a **GitHub App** (committer = `cairn-cms[bot]`, author = the editor) and auto-deploys.
8
7
 
9
- It is **design-agnostic**: each consumer site supplies an adapter (collections, slug
10
- convention, frontmatter schema, and its own `renderPreview(md)`), so the same engine drives
11
- sites with completely different markdown pipelines (e.g. [ecnordic.ski](https://ecnordic.ski)
12
- (remark→rehype directive pipeline) and [907.life](https://907.life) (plain `remark-html`)).
8
+ It is **design-agnostic**. Each consumer site supplies an adapter (the content contract through
9
+ `defineAdapter`/`defineFields`, the slug and permalink rules, and its render configuration), so the
10
+ same engine drives sites with completely different markdown pipelines. Two run in production today:
11
+ [ecnordic.ski](https://ecnordic.ski) (a remark-to-rehype directive pipeline) and
12
+ [907.life](https://907.life) (the engine's own `createRenderer`). Content is a fixed set of
13
+ first-class concepts (Posts and Pages), not open-ended collections.
13
14
 
14
15
  ## Status
15
16
 
16
- **`0.4.x`: auth on [better-auth](https://better-auth.com); API not yet frozen.** The core was
17
- built *inside ecnordic.ski first* (the richer proving ground) with the cairn-core ↔ site-adapter
18
- seams designed in from day one, then extracted into this package and validated on a second design
19
- (907.life). Editor auth runs on **better-auth (Cloudflare D1 + magic-link)** behind a scanner-safe
20
- **POST-confirm** flow, with two-tier `owner`/`editor` roles; the GitHub-App commit signer stays
21
- bespoke. The GitHub commit path, Carta preview, the adapter contract, and the shared admin shell
22
- (`/sveltekit` server logic + `/components` Svelte UI + `/auth`) all run on both sites. Pin a caret
23
- range and expect 0.x churn.
24
-
25
- > **Breaking in `0.4.0`** (from `0.3.x`): editor auth moved off the hand-rolled magic-link/KV/
26
- > signed-cookie stack onto better-auth. Each site now needs a **D1 binding** (`AUTH_DB`) +
27
- > committed migrations, an `AUTH_SECRET`, a `/api/auth/[...all]` catch-all + `/admin/auth/confirm`
28
- > shims, and the new `better-auth` + `drizzle-orm` peer deps. Magic links are now POST-confirm
29
- > (a confirm page, not a GET link).
17
+ cairn-cms runs two production sites today, [ecnordic.ski](https://ecnordic.ski) and
18
+ [907.life](https://907.life). It is `0.x` and breaks between minor versions. The author is
19
+ still working through the core-feature roadmap, and the project stays closely held until that
20
+ core lands. See the [ROADMAP](./ROADMAP.md) for what is planned and the
21
+ [CHANGELOG](./CHANGELOG.md) for what changed.
22
+
23
+ Editor auth is self-owned: an atomic single-use magic-link token, a POST-confirm flow, opaque
24
+ D1-backed session rows, and two-tier `owner`/`editor` roles. There is no better-auth, Drizzle,
25
+ or ORM. Pin a caret range and read the CHANGELOG before bumping; every breaking entry carries a
26
+ "Consumers must" line.
27
+
28
+ A contributor who feels inspired is welcome to open an issue or a discussion to start a
29
+ conversation. There is no formal contribution process yet, so this is not an open call for
30
+ pull requests.
30
31
 
31
32
  ## Install
32
33
 
@@ -34,23 +35,35 @@ range and expect 0.x churn.
34
35
  npm install @glw907/cairn-cms
35
36
  ```
36
37
 
37
- Peers: `svelte@^5`, `@sveltejs/kit@^2`, and `carta-md@^4.11` (the editor component). Each site
38
- implements a `CairnAdapter` (see `docs/PLAN.md`) and mounts thin `/admin` route shims around
39
- `@glw907/cairn-cms/sveltekit` (server logic) and `@glw907/cairn-cms/components` (the admin UI).
38
+ Peer dependencies: `svelte@^5` and `@sveltejs/kit@^2`. A consumer site implements a `CairnAdapter`
39
+ and mounts thin `/admin` route shims around the package subpaths:
40
40
 
41
- ## How it's developed
41
+ - `@glw907/cairn-cms`: the core engine and adapter contract.
42
+ - `@glw907/cairn-cms/sveltekit`: the server load and action logic.
43
+ - `@glw907/cairn-cms/components`: the admin Svelte UI.
44
+ - `@glw907/cairn-cms/delivery` and `/delivery/data`: the public read model (indexes, feeds,
45
+ sitemap, SEO head). The `/delivery/data` barrel is node-safe, with no `@sveltejs/kit` in its graph.
46
+ - `@glw907/cairn-cms/vite`: the `cairnManifest()` Vite plugin, paired with the `cairn-manifest` bin,
47
+ that builds and verifies the committed content manifest at build time.
42
48
 
43
- This repo lives in a dev meta-workspace alongside its consumer sites:
49
+ Each site binds a Cloudflare D1 database as `AUTH_DB` (the editor allowlist, sessions, and single-use
50
+ magic tokens) and a `[[send_email]]` binding named `EMAIL`. The worked reference for every shape is
51
+ `examples/showcase`.
44
52
 
45
- ```
46
- ~/Projects/cairn/ # npm workspace root (not a git repo)
47
- cairn-cms/ this repo
48
- ecnordic-ski/ ← consumer (first proving ground)
49
- 907-life/ ← consumer (second design, validates the abstraction)
50
- ```
53
+ ## Documentation
54
+
55
+ The [`docs/`](./docs/README.md) tree is organized in four arms: a tutorial that builds a first
56
+ site end to end, how-to guides for each setup task, a reference for every package export, and
57
+ explanation pages for the architecture and design rules. Start at the
58
+ [documentation index](./docs/README.md). The [security policy](./SECURITY.md) covers reporting
59
+ and the security posture.
60
+
61
+ ## How it's developed
51
62
 
52
- npm workspaces symlink `cairn-cms` into each site's `node_modules` for zero-publish local
53
- dev. In CI, each site pins a published version so deploys stay reproducible.
63
+ This is a standalone repo. Consumer sites install the published package from the npm registry by
64
+ version range. The library's own development proves changes against `examples/showcase`, a
65
+ self-contained SvelteKit site that consumes the package through the relative `file:../..` path, so a
66
+ change is exercised end to end before it publishes.
54
67
 
55
- See **`docs/PLAN.md`** for the full architecture, locked decisions, phased passes, and
56
- risk register.
68
+ The historical rebuild plan and the early architecture writeups live under `docs/internal/`.
69
+ They are kept for history and are not current.
@@ -16,4 +16,3 @@ export declare function generateToken(): string;
16
16
  export declare function generateSessionId(): string;
17
17
  /** The lowercase hex SHA-256 of a token, for storage and lookup. */
18
18
  export declare function hashToken(token: string): Promise<string>;
19
- //# sourceMappingURL=crypto.d.ts.map
@@ -40,4 +40,3 @@ export declare function setEditorRole(db: D1Database, email: string, role: Role)
40
40
  * `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
41
41
  */
42
42
  export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
43
- //# sourceMappingURL=store.d.ts.map
@@ -22,4 +22,3 @@ export interface AuthEnv {
22
22
  }): Promise<void>;
23
23
  };
24
24
  }
25
- //# sourceMappingURL=types.d.ts.map
@@ -16,4 +16,3 @@ interface Props {
16
16
  declare const AdminLayout: import("svelte").Component<Props, {}, "">;
17
17
  type AdminLayout = ReturnType<typeof AdminLayout>;
18
18
  export default AdminLayout;
19
- //# sourceMappingURL=AdminLayout.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const ComponentForm: import("svelte").Component<Props, {}, "">;
18
18
  type ComponentForm = ReturnType<typeof ComponentForm>;
19
19
  export default ComponentForm;
20
- //# sourceMappingURL=ComponentForm.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const ComponentInsertDialog: import("svelte").Component<Props, {}, "">;
18
18
  type ComponentInsertDialog = ReturnType<typeof ComponentInsertDialog>;
19
19
  export default ComponentInsertDialog;
20
- //# sourceMappingURL=ComponentInsertDialog.svelte.d.ts.map
@@ -10,4 +10,3 @@ interface Props {
10
10
  declare const ConceptList: import("svelte").Component<Props, {}, "">;
11
11
  type ConceptList = ReturnType<typeof ConceptList>;
12
12
  export default ConceptList;
13
- //# sourceMappingURL=ConceptList.svelte.d.ts.map
@@ -14,4 +14,3 @@ interface Props {
14
14
  declare const ConfirmPage: import("svelte").Component<Props, {}, "">;
15
15
  type ConfirmPage = ReturnType<typeof ConfirmPage>;
16
16
  export default ConfirmPage;
17
- //# sourceMappingURL=ConfirmPage.svelte.d.ts.map
@@ -18,4 +18,3 @@ interface Props {
18
18
  declare const DeleteDialog: import("svelte").Component<Props, {}, "">;
19
19
  type DeleteDialog = ReturnType<typeof DeleteDialog>;
20
20
  export default DeleteDialog;
21
- //# sourceMappingURL=DeleteDialog.svelte.d.ts.map
@@ -33,4 +33,3 @@ interface Props {
33
33
  declare const EditPage: import("svelte").Component<Props, {}, "">;
34
34
  type EditPage = ReturnType<typeof EditPage>;
35
35
  export default EditPage;
36
- //# sourceMappingURL=EditPage.svelte.d.ts.map
@@ -12,4 +12,3 @@ interface Props {
12
12
  declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
13
13
  type EditorToolbar = ReturnType<typeof EditorToolbar>;
14
14
  export default EditorToolbar;
15
- //# sourceMappingURL=EditorToolbar.svelte.d.ts.map
@@ -21,4 +21,3 @@ interface Props {
21
21
  declare const IconPicker: import("svelte").Component<Props, {}, "">;
22
22
  type IconPicker = ReturnType<typeof IconPicker>;
23
23
  export default IconPicker;
24
- //# sourceMappingURL=IconPicker.svelte.d.ts.map
@@ -15,4 +15,3 @@ interface Props {
15
15
  declare const LinkPicker: import("svelte").Component<Props, {}, "">;
16
16
  type LinkPicker = ReturnType<typeof LinkPicker>;
17
17
  export default LinkPicker;
18
- //# sourceMappingURL=LinkPicker.svelte.d.ts.map
@@ -18,4 +18,3 @@ interface Props {
18
18
  declare const LoginPage: import("svelte").Component<Props, {}, "">;
19
19
  type LoginPage = ReturnType<typeof LoginPage>;
20
20
  export default LoginPage;
21
- //# sourceMappingURL=LoginPage.svelte.d.ts.map
@@ -20,4 +20,3 @@ interface Props {
20
20
  declare const ManageEditors: import("svelte").Component<Props, {}, "">;
21
21
  type ManageEditors = ReturnType<typeof ManageEditors>;
22
22
  export default ManageEditors;
23
- //# sourceMappingURL=ManageEditors.svelte.d.ts.map
@@ -21,4 +21,3 @@ interface Props {
21
21
  declare const MarkdownEditor: import("svelte").Component<Props, {}, "value">;
22
22
  type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
23
23
  export default MarkdownEditor;
24
- //# sourceMappingURL=MarkdownEditor.svelte.d.ts.map
@@ -14,4 +14,3 @@ interface Props {
14
14
  declare const NavTree: import("svelte").Component<Props, {}, "">;
15
15
  type NavTree = ReturnType<typeof NavTree>;
16
16
  export default NavTree;
17
- //# sourceMappingURL=NavTree.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const RenameDialog: import("svelte").Component<Props, {}, "">;
18
18
  type RenameDialog = ReturnType<typeof RenameDialog>;
19
19
  export default RenameDialog;
20
- //# sourceMappingURL=RenameDialog.svelte.d.ts.map
@@ -12,4 +12,3 @@ export { default as NavTree } from './NavTree.svelte';
12
12
  export { default as LinkPicker } from './LinkPicker.svelte';
13
13
  export { default as DeleteDialog } from './DeleteDialog.svelte';
14
14
  export { default as RenameDialog } from './RenameDialog.svelte';
15
- //# sourceMappingURL=index.d.ts.map
@@ -13,4 +13,3 @@ export declare function linkCompletions(targets: LinkTarget[], query: string): C
13
13
  * whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
14
14
  * filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
15
15
  export declare function cairnLinkCompletionSource(targets: LinkTarget[]): CompletionSource;
16
- //# sourceMappingURL=link-completion.d.ts.map
@@ -30,4 +30,3 @@ export declare function unwrapCairnLink(doc: string, href: string): string;
30
30
  * last to first, replacing only the `](oldHref` run so the label and title stay exact.
31
31
  */
32
32
  export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
33
- //# sourceMappingURL=markdown-format.d.ts.map
@@ -1,4 +1,3 @@
1
1
  import type { CairnAdapter } from './types.js';
2
2
  /** Declare a site's adapter while preserving each concept's concrete schema type for typed reads. */
3
3
  export declare function defineAdapter<const A extends CairnAdapter>(adapter: A): A;
4
- //# sourceMappingURL=adapter.d.ts.map
@@ -1,7 +1,17 @@
1
- import type { CairnAdapter, CairnExtension, CairnRuntime, ConceptUrlPolicy } from './types.js';
1
+ import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
2
+ import type { SiteConfig } from '../nav/site-config.js';
3
+ /** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
4
+ * always derived from one source and can never be silently dropped. `extensions` fold in after the
5
+ * adapter's concepts. */
6
+ export interface ComposeInput {
7
+ adapter: CairnAdapter;
8
+ siteConfig: SiteConfig;
9
+ extensions?: CairnExtension[];
10
+ }
2
11
  /**
3
- * Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
4
- * merge after the adapter's. The asset slot (seam 4) passes through untouched.
12
+ * Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
13
+ * is derived from the site config, the same source the delivery path uses, so the runtime and
14
+ * delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
15
+ * (seam 4) passes through untouched.
5
16
  */
6
- export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[], urlPolicy?: Record<string, ConceptUrlPolicy | undefined>): CairnRuntime;
7
- //# sourceMappingURL=compose.d.ts.map
17
+ export declare function composeRuntime({ adapter, siteConfig, extensions }: ComposeInput): CairnRuntime;
@@ -1,9 +1,13 @@
1
- import { normalizeConcepts } from './concepts.js';
1
+ import { resolveConcepts } from './concepts.js';
2
2
  /**
3
- * Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
4
- * merge after the adapter's. The asset slot (seam 4) passes through untouched.
3
+ * Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
4
+ * is derived from the site config, the same source the delivery path uses, so the runtime and
5
+ * delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
6
+ * (seam 4) passes through untouched.
5
7
  */
6
- export function composeRuntime(adapter, extensions = [], urlPolicy = {}) {
8
+ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
9
+ if (!siteConfig)
10
+ throw new Error('composeRuntime needs a site config to derive the URL policy');
7
11
  const content = { ...adapter.content };
8
12
  const adminPanels = [];
9
13
  const fieldTypes = [];
@@ -19,7 +23,7 @@ export function composeRuntime(adapter, extensions = [], urlPolicy = {}) {
19
23
  }
20
24
  return {
21
25
  siteName: adapter.siteName,
22
- concepts: normalizeConcepts(content, urlPolicy),
26
+ concepts: resolveConcepts(content, siteConfig),
23
27
  backend: adapter.backend,
24
28
  sender: adapter.sender,
25
29
  render: adapter.render,
@@ -1,4 +1,5 @@
1
1
  import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
2
+ import { type SiteConfig } from '../nav/site-config.js';
2
3
  /**
3
4
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
4
5
  * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
@@ -13,6 +14,11 @@ export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
13
14
  * a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
14
15
  */
15
16
  export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
17
+ /**
18
+ * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
19
+ * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
20
+ * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
21
+ */
22
+ export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
16
23
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
17
24
  export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
18
- //# sourceMappingURL=concepts.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { urlPolicyFrom } from '../nav/site-config.js';
1
2
  /**
2
3
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
3
4
  * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
@@ -17,6 +18,37 @@ function defaultLabel(id) {
17
18
  function defaultPermalink(id) {
18
19
  return id === 'pages' ? '/:slug' : `/${id}/:slug`;
19
20
  }
21
+ /** Permalink tokens the resolver understands. */
22
+ const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
23
+ /** The date-bearing tokens; valid only for a dated concept. */
24
+ const DATE_TOKENS = new Set(['year', 'month', 'day']);
25
+ /** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
26
+ const DATE_PREFIXES = new Set(['year', 'month', 'day']);
27
+ /**
28
+ * Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
29
+ * here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
30
+ * use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
31
+ */
32
+ function validateUrlPolicy(id, policy, dated) {
33
+ if (policy.permalink !== undefined) {
34
+ const pattern = policy.permalink;
35
+ if (!pattern.startsWith('/')) {
36
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
37
+ }
38
+ for (const match of pattern.matchAll(/:(\w+)/g)) {
39
+ const token = match[1];
40
+ if (!KNOWN_TOKENS.has(token)) {
41
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
42
+ }
43
+ if (DATE_TOKENS.has(token) && !dated) {
44
+ throw new Error(`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`);
45
+ }
46
+ }
47
+ }
48
+ if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
49
+ throw new Error(`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`);
50
+ }
51
+ }
20
52
  /**
21
53
  * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
22
54
  * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
@@ -26,6 +58,12 @@ function defaultPermalink(id) {
26
58
  */
27
59
  export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
28
60
  const descriptors = [];
61
+ const declaredConcepts = new Set(Object.keys(content).filter((key) => content[key] !== undefined));
62
+ for (const key of Object.keys(urlPolicy)) {
63
+ if (!declaredConcepts.has(key)) {
64
+ throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
65
+ }
66
+ }
29
67
  for (const [id, config] of Object.entries(content)) {
30
68
  if (!config)
31
69
  continue;
@@ -35,12 +73,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
35
73
  if (undeclared !== undefined) {
36
74
  throw new Error(`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`);
37
75
  }
76
+ const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
38
77
  const policy = urlPolicy[id] ?? {};
78
+ validateUrlPolicy(id, policy, conceptRouting.dated);
39
79
  descriptors.push({
40
80
  id,
41
81
  label: config.label ?? defaultLabel(id),
42
82
  dir: config.dir,
43
- routing: routing[id] ?? DEFAULT_ROUTING,
83
+ routing: conceptRouting,
44
84
  permalink: policy.permalink ?? defaultPermalink(id),
45
85
  datePrefix: policy.datePrefix ?? 'day',
46
86
  fields: config.schema.fields,
@@ -50,6 +90,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
50
90
  }
51
91
  return descriptors;
52
92
  }
93
+ /**
94
+ * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
95
+ * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
96
+ * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
97
+ */
98
+ export function resolveConcepts(content, siteConfig) {
99
+ return normalizeConcepts(content, urlPolicyFrom(siteConfig));
100
+ }
53
101
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
54
102
  export function findConcept(concepts, id) {
55
103
  return concepts.find((concept) => concept.id === id);
@@ -23,4 +23,3 @@ export declare function parseMarkdown(source: string): {
23
23
  frontmatter: Record<string, unknown>;
24
24
  body: string;
25
25
  };
26
- //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1,23 @@
1
+ import type { ConceptDescriptor } from './types.js';
2
+ /** A content entry's resolved URL identity. */
3
+ export interface EntryIdentity {
4
+ id: string;
5
+ slug: string;
6
+ date?: string;
7
+ permalink: string;
8
+ }
9
+ /** A present, non-empty string, else undefined. The read-model string coercion. */
10
+ export declare function asString(value: unknown): string | undefined;
11
+ /** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
12
+ export declare function asDate(value: unknown): string | undefined;
13
+ /** Tags as an array, empty when the file declares none. */
14
+ export declare function asTags(value: unknown): string[];
15
+ /** A content entry's id: its filename stem (the date prefix is part of a dated id). */
16
+ export declare function entryId(path: string): string;
17
+ /**
18
+ * Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
19
+ * frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
20
+ * an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
21
+ * once and passes the frontmatter, so there is no second parse here.
22
+ */
23
+ export declare function entryIdentity(descriptor: ConceptDescriptor, path: string, frontmatter: Record<string, unknown>): EntryIdentity;
@@ -0,0 +1,43 @@
1
+ // cairn-cms: a content entry's URL identity in one place (engine-hardening pass 3). The id, the
2
+ // slug, the date, and the permalink are computed here, so the content index and the manifest cannot
3
+ // drift on what an entry's URL is. A cairn: link resolves through the manifest in the admin preview
4
+ // and through the content index in the public build, so the two must agree by construction.
5
+ import { idFromFilename, slugFromId } from './ids.js';
6
+ import { permalink } from './permalink.js';
7
+ /** The basename of a glob path: the segment after the last slash, or the whole path. */
8
+ function basename(path) {
9
+ const slash = path.lastIndexOf('/');
10
+ return slash >= 0 ? path.slice(slash + 1) : path;
11
+ }
12
+ /** A present, non-empty string, else undefined. The read-model string coercion. */
13
+ export function asString(value) {
14
+ return typeof value === 'string' && value.trim() ? value : undefined;
15
+ }
16
+ /** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
17
+ export function asDate(value) {
18
+ if (value instanceof Date)
19
+ return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
20
+ if (typeof value === 'string')
21
+ return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
22
+ return undefined;
23
+ }
24
+ /** Tags as an array, empty when the file declares none. */
25
+ export function asTags(value) {
26
+ return Array.isArray(value) ? value.map(String) : [];
27
+ }
28
+ /** A content entry's id: its filename stem (the date prefix is part of a dated id). */
29
+ export function entryId(path) {
30
+ return idFromFilename(basename(path));
31
+ }
32
+ /**
33
+ * Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
34
+ * frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
35
+ * an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
36
+ * once and passes the frontmatter, so there is no second parse here.
37
+ */
38
+ export function entryIdentity(descriptor, path, frontmatter) {
39
+ const id = entryId(path);
40
+ const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
41
+ const date = asDate(frontmatter.date);
42
+ return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
43
+ }
@@ -35,4 +35,3 @@ export declare function composeDatedId(date: string, slug: string, datePrefix: D
35
35
  * newSlug. The caller validates newSlug with isValidId first.
36
36
  */
37
37
  export declare function renameId(oldId: string, newSlug: string, datePrefix: DatePrefix | null): string;
38
- //# sourceMappingURL=ids.d.ts.map