@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
@@ -18,4 +18,3 @@ export declare function escapeLinkText(text: string): string;
18
18
  /** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
19
19
  * Parses the body as mdast, so a token inside a code span or fence is never matched. */
20
20
  export declare function extractCairnLinks(body: string): CairnRef[];
21
- //# sourceMappingURL=links.d.ts.map
@@ -24,7 +24,9 @@ export interface LinkTarget {
24
24
  date?: string;
25
25
  draft: boolean;
26
26
  }
27
- /** Build one manifest entry from a content file. Drafts are included and flagged. */
27
+ /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
28
+ * permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
29
+ * one URL whether the admin preview reads the manifest or the public build reads the content index. */
28
30
  export declare function manifestEntryFromFile(descriptor: ConceptDescriptor, file: {
29
31
  path: string;
30
32
  raw: string;
@@ -39,9 +41,26 @@ export declare function serializeManifest(manifest: Manifest): string;
39
41
  * error. The build regenerates the manifest, so a real file is always canonical; this guards a
40
42
  * hand-edited or truncated one. */
41
43
  export declare function parseManifest(raw: string): Manifest;
42
- /** Throw if the committed manifest drifts from what the corpus says. Both sides are compared in the
43
- * canonical serialized form, so semantic equality never spuriously fails. The build calls this so a
44
- * raw-git content edit, which leaves the committed manifest stale, fails the build loudly. */
44
+ /** A changed entry and the fields that differ between the built and committed manifests. */
45
+ export interface ManifestEntryDiff {
46
+ concept: string;
47
+ id: string;
48
+ fields: string[];
49
+ }
50
+ /** The drift between a freshly built manifest and the committed one, keyed by concept+id. */
51
+ export interface ManifestDiff {
52
+ added: ManifestEntry[];
53
+ removed: ManifestEntry[];
54
+ changed: ManifestEntryDiff[];
55
+ }
56
+ /** Compare a built manifest against a committed one, keyed by concept+id (the same identity
57
+ * upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
58
+ * unit-tested apart from any build. */
59
+ export declare function diffManifests(built: Manifest, committed: Manifest): ManifestDiff;
60
+ /** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
61
+ * is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
62
+ * error names the added, removed, and changed entries, so a raw-git content edit that leaves the
63
+ * committed manifest stale fails the build loudly with what drifted. */
45
64
  export declare function verifyManifest(built: Manifest, committedRaw: string): void;
46
65
  /** Replace the entry with the same concept and id, or add it. Order does not matter, since
47
66
  * serializeManifest sorts. This is the save path's incremental patch. */
@@ -66,4 +85,3 @@ export declare function manifestLinkResolver(targets: {
66
85
  id: string;
67
86
  permalink: string;
68
87
  }[]): LinkResolve;
69
- //# sourceMappingURL=manifest.d.ts.map
@@ -3,41 +3,21 @@
3
3
  // code reads the content graph without an N+1 GitHub crawl. The build regenerates and verifies
4
4
  // it; the save path patches one entry and commits it with the content in one commit. Each entry
5
5
  // carries its identity and its outbound cairn: edges, so the manifest is the link graph.
6
- import { idFromFilename, slugFromId } from './ids.js';
7
6
  import { parseMarkdown } from './frontmatter.js';
8
- import { permalink } from './permalink.js';
7
+ import { entryIdentity, asString } from './identity.js';
9
8
  import { extractCairnLinks } from './links.js';
10
- function basename(path) {
11
- const slash = path.lastIndexOf('/');
12
- return slash >= 0 ? path.slice(slash + 1) : path;
13
- }
14
- /** Mirror content-index's frontmatter coercion: a present non-empty string, else undefined. */
15
- function asString(value) {
16
- return typeof value === 'string' && value.trim() ? value : undefined;
17
- }
18
- /** Mirror content-index's date coercion: an unquoted YAML date is a JS Date, a string is sliced. */
19
- function asDate(value) {
20
- if (value instanceof Date)
21
- return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
22
- if (typeof value === 'string')
23
- return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
24
- return undefined;
25
- }
26
- /** Build one manifest entry from a content file. Drafts are included and flagged. */
9
+ /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
10
+ * permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
11
+ * one URL whether the admin preview reads the manifest or the public build reads the content index. */
27
12
  export function manifestEntryFromFile(descriptor, file) {
28
- const id = idFromFilename(basename(file.path));
29
- // Use the same slug rule content-index uses, so the manifest's permalink for an entry always
30
- // equals content-index's permalink for it. A cairn link must resolve to one URL whether the
31
- // admin preview reads the manifest or the public build reads the content index.
32
- const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
33
13
  const { frontmatter, body } = parseMarkdown(file.raw);
34
- const date = asDate(frontmatter.date);
14
+ const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
35
15
  return {
36
16
  id,
37
17
  concept: descriptor.id,
38
18
  title: asString(frontmatter.title) ?? id,
39
19
  date,
40
- permalink: permalink(descriptor, { id, slug, date }),
20
+ permalink,
41
21
  draft: frontmatter.draft === true,
42
22
  links: extractCairnLinks(body),
43
23
  };
@@ -104,13 +84,56 @@ export function parseManifest(raw) {
104
84
  }
105
85
  return { version: 1, entries: obj.entries };
106
86
  }
107
- /** Throw if the committed manifest drifts from what the corpus says. Both sides are compared in the
108
- * canonical serialized form, so semantic equality never spuriously fails. The build calls this so a
109
- * raw-git content edit, which leaves the committed manifest stale, fails the build loudly. */
110
- export function verifyManifest(built, committedRaw) {
111
- if (committedRaw !== serializeManifest(built)) {
112
- throw new Error('content manifest is stale: the committed file does not match the corpus. Regenerate it (npm run cairn:manifest) and commit the result.');
87
+ const keyOf = (e) => `${e.concept}/${e.id}`;
88
+ /** Compare a built manifest against a committed one, keyed by concept+id (the same identity
89
+ * upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
90
+ * unit-tested apart from any build. */
91
+ export function diffManifests(built, committed) {
92
+ const builtByKey = new Map(built.entries.map((e) => [keyOf(e), e]));
93
+ const committedByKey = new Map(committed.entries.map((e) => [keyOf(e), e]));
94
+ const added = built.entries.filter((e) => !committedByKey.has(keyOf(e)));
95
+ const removed = committed.entries.filter((e) => !builtByKey.has(keyOf(e)));
96
+ const changed = [];
97
+ for (const b of built.entries) {
98
+ const c = committedByKey.get(keyOf(b));
99
+ if (!c)
100
+ continue;
101
+ // ManifestEntry has no index signature, so read its keys through an unknown-cast record.
102
+ const br = b;
103
+ const cr = c;
104
+ const fields = [...new Set([...Object.keys(b), ...Object.keys(c)])].filter((k) => JSON.stringify(br[k]) !== JSON.stringify(cr[k]));
105
+ if (fields.length > 0)
106
+ changed.push({ concept: b.concept, id: b.id, fields });
113
107
  }
108
+ return { added, removed, changed };
109
+ }
110
+ /** Format a diff into a short human-readable block for a build error. */
111
+ function formatDiff(d) {
112
+ const lines = [];
113
+ for (const e of d.added)
114
+ lines.push(` + ${keyOf(e)}`);
115
+ for (const e of d.removed)
116
+ lines.push(` - ${keyOf(e)}`);
117
+ for (const e of d.changed)
118
+ lines.push(` ~ ${e.concept}/${e.id} (${e.fields.join(', ')})`);
119
+ return lines.join('\n');
120
+ }
121
+ /** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
122
+ * is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
123
+ * error names the added, removed, and changed entries, so a raw-git content edit that leaves the
124
+ * committed manifest stale fails the build loudly with what drifted. */
125
+ export function verifyManifest(built, committedRaw) {
126
+ const builtRaw = serializeManifest(built);
127
+ if (committedRaw === builtRaw)
128
+ return;
129
+ // Diff the canonical built form, not the raw one. serializeManifest sorts each entry's links, so a
130
+ // build whose links are in extraction order would otherwise report a false (links) drift for an
131
+ // entry whose link set is identical and only the order differs. Reuse the serialized form so both
132
+ // sides are canonical.
133
+ const diff = diffManifests(parseManifest(builtRaw), parseManifest(committedRaw));
134
+ throw new Error('content manifest is stale: the committed file does not match the corpus.\n' +
135
+ formatDiff(diff) +
136
+ '\nRegenerate it (npm run cairn:manifest) and commit the result.');
114
137
  }
115
138
  /** Replace the entry with the same concept and id, or add it. Order does not matter, since
116
139
  * serializeManifest sorts. This is the save path's incremental patch. */
@@ -9,4 +9,3 @@ export declare function permalink(descriptor: ConceptDescriptor, entry: {
9
9
  slug: string;
10
10
  date?: string;
11
11
  }): string;
12
- //# sourceMappingURL=permalink.d.ts.map
@@ -72,4 +72,3 @@ export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
72
72
  /** Declare a concept's fields once. Returns the schema's faces derived from that one declaration. */
73
73
  export declare function defineFields<const F extends readonly FrontmatterField[]>(fields: F, options?: DefineFieldsOptions<F>): ConceptSchema<F>;
74
74
  export {};
75
- //# sourceMappingURL=schema.d.ts.map
@@ -270,4 +270,3 @@ export interface CairnRuntime {
270
270
  fieldTypes?: FieldTypeDef[];
271
271
  }
272
272
  export {};
273
- //# sourceMappingURL=types.d.ts.map
@@ -3,7 +3,10 @@ import type { FrontmatterField, ValidationResult } from './types.js';
3
3
  * Validate raw frontmatter against a field list. Required text and date fields must be
4
4
  * non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
5
5
  * and an unchecked one is omitted; a present tag field coerces to a string array and an empty
6
- * one is omitted; an empty optional text or date field is omitted, so the normalized data
6
+ * one is omitted, so validated data carries no key for an absent tag field (`tags` or `freetags`).
7
+ * The delivery read model
8
+ * (`ContentSummary.tags`) fills that case with an empty array; the two layers differ on purpose.
9
+ * An empty optional text or date field is omitted, so the normalized data
7
10
  * carries only meaningful values and committed frontmatter stays minimal. Returns the
8
11
  * normalized data, or field-keyed errors when any required field is empty.
9
12
  *
@@ -12,4 +15,3 @@ import type { FrontmatterField, ValidationResult } from './types.js';
12
15
  * `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
13
16
  */
14
17
  export declare function validateFields(fields: FrontmatterField[], frontmatter: Record<string, unknown>): ValidationResult;
15
- //# sourceMappingURL=validate.d.ts.map
@@ -3,7 +3,10 @@ import { dateInputValue, isCalendarDate } from './frontmatter.js';
3
3
  * Validate raw frontmatter against a field list. Required text and date fields must be
4
4
  * non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
5
5
  * and an unchecked one is omitted; a present tag field coerces to a string array and an empty
6
- * one is omitted; an empty optional text or date field is omitted, so the normalized data
6
+ * one is omitted, so validated data carries no key for an absent tag field (`tags` or `freetags`).
7
+ * The delivery read model
8
+ * (`ContentSummary.tags`) fills that case with an empty array; the two layers differ on purpose.
9
+ * An empty optional text or date field is omitted, so the normalized data
7
10
  * carries only meaningful values and committed frontmatter stays minimal. Returns the
8
11
  * normalized data, or field-keyed errors when any required field is empty.
9
12
  *
@@ -12,4 +12,3 @@ type $$ComponentProps = {
12
12
  declare const CairnHead: import("svelte").Component<$$ComponentProps, {}, "">;
13
13
  type CairnHead = ReturnType<typeof CairnHead>;
14
14
  export default CairnHead;
15
- //# sourceMappingURL=CairnHead.svelte.d.ts.map
@@ -15,6 +15,10 @@ export interface ContentSummary {
15
15
  title: string;
16
16
  date?: string;
17
17
  updated?: string;
18
+ /** The entry's tags, always present as an array and empty when the file declares none. This is the
19
+ * read-model normalization. It differs on purpose from the validated `frontmatter.tags`, which the
20
+ * validator omits when empty, so a published file carries no `tags: []` noise. Read `tags` here for
21
+ * a list; read `frontmatter.tags` only when you need the validated, possibly-absent value. */
18
22
  tags: string[];
19
23
  excerpt: string;
20
24
  wordCount: number;
@@ -61,4 +65,3 @@ export interface ContentIndex<F = Record<string, unknown>> {
61
65
  export declare function fromGlob(record: Record<string, string>): RawFile[];
62
66
  /** Build a concept's index from its raw files and normalized descriptor. */
63
67
  export declare function createContentIndex<F = Record<string, unknown>>(files: RawFile[], descriptor: ConceptDescriptor): ContentIndex<F>;
64
- //# sourceMappingURL=content-index.d.ts.map
@@ -3,47 +3,30 @@
3
3
  // returns cheap plain-data summaries plus an on-demand detail lookup. It is concept-generic:
4
4
  // every operation reads the descriptor and its routing rule, never a hardcoded concept id.
5
5
  import { parseMarkdown } from '../content/frontmatter.js';
6
- import { idFromFilename, slugFromId } from '../content/ids.js';
7
- import { permalink } from '../content/permalink.js';
6
+ import { entryId, entryIdentity, asDate, asString, asTags } from '../content/identity.js';
8
7
  import { deriveExcerpt, wordCount } from './excerpt.js';
9
8
  /** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
10
9
  export function fromGlob(record) {
11
10
  return Object.entries(record).map(([path, raw]) => ({ path, raw }));
12
11
  }
13
- function basename(path) {
14
- const slash = path.lastIndexOf('/');
15
- return slash >= 0 ? path.slice(slash + 1) : path;
16
- }
17
- function asString(value) {
18
- return typeof value === 'string' && value.trim() ? value : undefined;
19
- }
20
- function asDate(value) {
21
- if (value instanceof Date)
22
- return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
23
- if (typeof value === 'string')
24
- return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
25
- return undefined;
26
- }
27
- function asTags(value) {
28
- return Array.isArray(value) ? value.map(String) : [];
29
- }
30
12
  /** Build a concept's index from its raw files and normalized descriptor. */
31
13
  export function createContentIndex(files, descriptor) {
32
14
  const problems = [];
33
15
  const entries = [];
34
16
  for (const file of files) {
35
- const id = idFromFilename(basename(file.path));
36
- const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
37
17
  const { frontmatter: raw, body } = parseMarkdown(file.raw);
38
- const date = asDate(raw.date);
18
+ const id = entryId(file.path);
39
19
  const draft = raw.draft === true;
40
- // Validate once at build. A failure is recorded for the site gate and excluded from the typed
41
- // read, so every readable entry's frontmatter is the validator's normalized output, never raw.
20
+ // Validate before resolving the permalink. A date-token permalink throws on an entry with no
21
+ // valid date; the validate gate records that as a content problem rather than aborting the whole
22
+ // index build, so one bad entry degrades to a skip, not a crash. A failure is also excluded from
23
+ // the typed read, so every readable entry's frontmatter is the validator's normalized output.
42
24
  const result = descriptor.validate(raw, body);
43
25
  if (!result.ok) {
44
26
  problems.push({ id, draft, errors: result.errors });
45
27
  continue;
46
28
  }
29
+ const { slug, date, permalink } = entryIdentity(descriptor, file.path, raw);
47
30
  const summaryFieldValues = {};
48
31
  for (const key of descriptor.summaryFields) {
49
32
  if (key in result.data)
@@ -53,7 +36,7 @@ export function createContentIndex(files, descriptor) {
53
36
  concept: descriptor.id,
54
37
  id,
55
38
  slug,
56
- permalink: permalink(descriptor, { id, slug, date }),
39
+ permalink,
57
40
  title: asString(raw.title) ?? id,
58
41
  date,
59
42
  updated: asDate(raw.updated),
@@ -0,0 +1,23 @@
1
+ export { createContentIndex, fromGlob } from './content-index.js';
2
+ export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
3
+ export { createSiteIndex } from './site-index.js';
4
+ export type { SiteIndex, ConceptIndex } from './site-index.js';
5
+ export { createSiteIndexes } from './site-indexes.js';
6
+ export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
7
+ export { siteDescriptors } from './site-descriptors.js';
8
+ export { deriveExcerpt, wordCount } from './excerpt.js';
9
+ export { buildRssFeed, buildJsonFeed } from './feeds.js';
10
+ export type { FeedChannel, FeedItem } from './feeds.js';
11
+ export { buildSitemap } from './sitemap.js';
12
+ export type { SitemapUrl } from './sitemap.js';
13
+ export { buildRobots } from './robots.js';
14
+ export { buildSeoMeta } from './seo.js';
15
+ export type { SeoInput, SeoMeta } from './seo.js';
16
+ export { readSeoFields, resolveImageUrl } from './seo-fields.js';
17
+ export type { SeoFields } from './seo-fields.js';
18
+ export { paginate } from './paginate.js';
19
+ export type { Page } from './paginate.js';
20
+ export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
21
+ export { jsonLdScript } from './json-ld.js';
22
+ export { permalink } from '../content/permalink.js';
23
+ export { buildSiteManifest, buildLinkResolver } from './manifest.js';
@@ -0,0 +1,18 @@
1
+ // cairn-cms: the node-safe delivery data surface (@glw907/cairn-cms/delivery/data). The pure corpus
2
+ // projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
3
+ // the graph. The full ./delivery barrel re-exports this and adds the route loaders.
4
+ export { createContentIndex, fromGlob } from './content-index.js';
5
+ export { createSiteIndex } from './site-index.js';
6
+ export { createSiteIndexes } from './site-indexes.js';
7
+ export { siteDescriptors } from './site-descriptors.js';
8
+ export { deriveExcerpt, wordCount } from './excerpt.js';
9
+ export { buildRssFeed, buildJsonFeed } from './feeds.js';
10
+ export { buildSitemap } from './sitemap.js';
11
+ export { buildRobots } from './robots.js';
12
+ export { buildSeoMeta } from './seo.js';
13
+ export { readSeoFields, resolveImageUrl } from './seo-fields.js';
14
+ export { paginate } from './paginate.js';
15
+ export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
16
+ export { jsonLdScript } from './json-ld.js';
17
+ export { permalink } from '../content/permalink.js';
18
+ export { buildSiteManifest, buildLinkResolver } from './manifest.js';
@@ -8,4 +8,3 @@ export declare function deriveExcerpt(body: string, opts?: {
8
8
  }): string;
9
9
  /** Count words in the stripped body. */
10
10
  export declare function wordCount(body: string): number;
11
- //# sourceMappingURL=excerpt.d.ts.map
@@ -24,4 +24,3 @@ export interface FeedItem {
24
24
  export declare function buildRssFeed(channel: FeedChannel, items: FeedItem[]): string;
25
25
  /** Build a JSON Feed 1.1 document. */
26
26
  export declare function buildJsonFeed(channel: FeedChannel, items: FeedItem[]): string;
27
- //# sourceMappingURL=feeds.d.ts.map
@@ -1,2 +1 @@
1
1
  export { default as CairnHead } from './CairnHead.svelte';
2
- //# sourceMappingURL=head.d.ts.map
@@ -1,26 +1,3 @@
1
- export { createContentIndex, fromGlob } from './content-index.js';
2
- export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
3
- export { createSiteIndex } from './site-index.js';
4
- export type { SiteIndex, ConceptIndex } from './site-index.js';
5
- export { createSiteIndexes } from './site-indexes.js';
6
- export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
7
- export { siteDescriptors } from './site-descriptors.js';
8
- export { deriveExcerpt, wordCount } from './excerpt.js';
9
- export { buildRssFeed, buildJsonFeed } from './feeds.js';
10
- export type { FeedChannel, FeedItem } from './feeds.js';
11
- export { buildSitemap } from './sitemap.js';
12
- export type { SitemapUrl } from './sitemap.js';
13
- export { buildRobots } from './robots.js';
14
- export { buildSeoMeta } from './seo.js';
15
- export type { SeoInput, SeoMeta } from './seo.js';
16
- export { readSeoFields, resolveImageUrl } from './seo-fields.js';
17
- export type { SeoFields } from './seo-fields.js';
18
- export { paginate } from './paginate.js';
19
- export type { Page } from './paginate.js';
20
- export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
21
- export { jsonLdScript } from './json-ld.js';
22
- export { permalink } from '../content/permalink.js';
23
- export { buildSiteManifest, buildLinkResolver } from './manifest.js';
1
+ export * from './data.js';
24
2
  export { createPublicRoutes } from '../sveltekit/public-routes.js';
25
3
  export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '../sveltekit/public-routes.js';
26
- //# sourceMappingURL=index.d.ts.map
@@ -1,21 +1,6 @@
1
- // cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The complete, canonical,
2
- // backend-free toolkit a SvelteKit site wires its public pages with: the content index and the
3
- // site resolver, the descriptor helper, the syndication and SEO builders, the endpoint response
4
- // helpers, the catch-all route loaders, and the head component. It imports nothing from auth,
5
- // github, or email, so importing it does not pull the server backend into a public bundle.
6
- export { createContentIndex, fromGlob } from './content-index.js';
7
- export { createSiteIndex } from './site-index.js';
8
- export { createSiteIndexes } from './site-indexes.js';
9
- export { siteDescriptors } from './site-descriptors.js';
10
- export { deriveExcerpt, wordCount } from './excerpt.js';
11
- export { buildRssFeed, buildJsonFeed } from './feeds.js';
12
- export { buildSitemap } from './sitemap.js';
13
- export { buildRobots } from './robots.js';
14
- export { buildSeoMeta } from './seo.js';
15
- export { readSeoFields, resolveImageUrl } from './seo-fields.js';
16
- export { paginate } from './paginate.js';
17
- export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
18
- export { jsonLdScript } from './json-ld.js';
19
- export { permalink } from '../content/permalink.js';
20
- export { buildSiteManifest, buildLinkResolver } from './manifest.js';
1
+ // cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The node-safe data surface
2
+ // (re-exported from ./delivery/data) plus the SvelteKit catch-all route loaders. The head component
3
+ // lives at ./delivery/head. Importing this pulls @sveltejs/kit through the route loaders, so a
4
+ // plain-Node tool imports from ./delivery/data instead.
5
+ export * from './data.js';
21
6
  export { createPublicRoutes } from '../sveltekit/public-routes.js';
@@ -1,2 +1 @@
1
1
  export declare function jsonLdScript(data: Record<string, unknown>): string;
2
- //# sourceMappingURL=json-ld.d.ts.map
@@ -10,4 +10,3 @@ export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, co
10
10
  /** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
11
11
  * fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
12
12
  export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
13
- //# sourceMappingURL=manifest.d.ts.map
@@ -10,4 +10,3 @@ export interface Page<T> {
10
10
  }
11
11
  /** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
12
12
  export declare function paginate<T>(items: T[], page: number, perPage: number): Page<T>;
13
- //# sourceMappingURL=paginate.d.ts.map
@@ -11,4 +11,3 @@ export declare function robotsResponse(opts: {
11
11
  sitemapUrl: string;
12
12
  disallow?: string[];
13
13
  }): Response;
14
- //# sourceMappingURL=responses.d.ts.map
@@ -3,4 +3,3 @@ export declare function buildRobots(opts: {
3
3
  sitemapUrl: string;
4
4
  disallow?: string[];
5
5
  }): string;
6
- //# sourceMappingURL=robots.d.ts.map
@@ -19,4 +19,3 @@ export declare function readSeoFields(frontmatter: Record<string, unknown>): Seo
19
19
  * bare path also anchors to the origin root; against a sub-path origin it would resolve relative to
20
20
  * that path, per the WHATWG URL rules. */
21
21
  export declare function resolveImageUrl(image: string, origin: string): string | undefined;
22
- //# sourceMappingURL=seo-fields.d.ts.map
@@ -35,4 +35,3 @@ export interface SeoMeta {
35
35
  }
36
36
  /** Build the head data for a page. */
37
37
  export declare function buildSeoMeta(input: SeoInput): SeoMeta;
38
- //# sourceMappingURL=seo.d.ts.map
@@ -2,4 +2,3 @@ import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
2
2
  import type { SiteConfig } from '../nav/site-config.js';
3
3
  /** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
4
4
  export declare function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[];
5
- //# sourceMappingURL=site-descriptors.d.ts.map
@@ -1,9 +1,8 @@
1
- // cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept
2
- // descriptors the admin runtime uses; this wraps the two calls that derive them so the
3
- // pairing is not tribal knowledge. The YAML URL policy stays the single source of truth.
4
- import { normalizeConcepts } from '../content/concepts.js';
5
- import { urlPolicyFrom } from '../nav/site-config.js';
1
+ // cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept descriptors
2
+ // the admin runtime uses; this delegates to the shared resolveConcepts so the pairing is one path, not
3
+ // tribal knowledge. The YAML URL policy stays the single source of truth.
4
+ import { resolveConcepts } from '../content/concepts.js';
6
5
  /** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
7
6
  export function siteDescriptors(adapter, siteConfig) {
8
- return normalizeConcepts(adapter.content, urlPolicyFrom(siteConfig));
7
+ return resolveConcepts(adapter.content, siteConfig);
9
8
  }
@@ -31,4 +31,3 @@ export interface SiteIndex {
31
31
  export declare function createSiteIndex(concepts: ConceptIndex[], opts?: {
32
32
  validate?: boolean;
33
33
  }): SiteIndex;
34
- //# sourceMappingURL=site-index.d.ts.map
@@ -23,4 +23,3 @@ export type SiteIndexes<A extends CairnAdapter> = {
23
23
  export declare function createSiteIndexes<const A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>, opts?: {
24
24
  validate?: boolean;
25
25
  }): SiteIndexes<A>;
26
- //# sourceMappingURL=site-indexes.d.ts.map
@@ -5,4 +5,3 @@ export interface SitemapUrl {
5
5
  }
6
6
  /** Build a sitemap XML document from a list of URLs. */
7
7
  export declare function buildSitemap(urls: SitemapUrl[]): string;
8
- //# sourceMappingURL=sitemap.d.ts.map
package/dist/email.d.ts CHANGED
@@ -24,4 +24,3 @@ export declare function buildMagicLinkMessage(input: {
24
24
  }): MagicLinkMessage;
25
25
  /** The production send: Cloudflare Email Sending through the EMAIL binding. */
26
26
  export declare const cloudflareSend: SendMagicLink;
27
- //# sourceMappingURL=email.d.ts.map
package/dist/env.d.ts CHANGED
@@ -21,4 +21,3 @@ export declare function requireOrigin(env: {
21
21
  export declare function requireDb(env: {
22
22
  AUTH_DB?: D1Database;
23
23
  }): D1Database;
24
- //# sourceMappingURL=env.d.ts.map
@@ -9,4 +9,3 @@ export interface GithubKeyEnv {
9
9
  * installation) and the Worker's private-key secret. Throws when the secret is unset.
10
10
  */
11
11
  export declare function appCredentials(backend: Pick<BackendConfig, 'appId' | 'installationId'>, env: GithubKeyEnv): AppCredentials;
12
- //# sourceMappingURL=credentials.d.ts.map
@@ -67,4 +67,3 @@ export declare function commitFiles(repo: RepoRef, changes: FileChange[], opts:
67
67
  author: CommitAuthor;
68
68
  }, token: string): Promise<string>;
69
69
  export {};
70
- //# sourceMappingURL=repo.d.ts.map
@@ -26,4 +26,3 @@ export declare function signingSelfTest(appId: string, privateKeyB64: string): P
26
26
  ok: boolean;
27
27
  detail?: string;
28
28
  }>;
29
- //# sourceMappingURL=signing.d.ts.map
@@ -32,4 +32,3 @@ export declare class CommitConflictError extends Error {
32
32
  readonly path: string;
33
33
  constructor(path: string);
34
34
  }
35
- //# sourceMappingURL=types.d.ts.map
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { buildMagicLinkMessage, cloudflareSend } from './email.js';
5
5
  export type { CairnAdapter, ConceptConfig, FrontmatterField, TextField, TextareaField, DateField, BooleanField, TagsField, FreeTagsField, ValidationResult, BackendConfig, SenderConfig, NavMenuConfig, AssetConfig, RoutingRule, ConceptDescriptor, ConceptUrlPolicy, CairnExtension, CairnRuntime, AdminPanel, FieldTypeDef, } from './content/types.js';
6
6
  export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
7
7
  export { composeRuntime } from './content/compose.js';
8
+ export type { ComposeInput } from './content/compose.js';
8
9
  export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
9
10
  export { defineFields } from './content/schema.js';
10
11
  export { defineAdapter } from './content/adapter.js';
@@ -13,8 +14,8 @@ export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, compose
13
14
  export type { DatePrefix } from './content/ids.js';
14
15
  export { parseCairnToken, extractCairnLinks, formatCairnToken, escapeLinkText } from './content/links.js';
15
16
  export type { CairnRef, LinkResolve } from './content/links.js';
16
- export { serializeManifest, parseManifest, emptyManifest, verifyManifest, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
17
- export type { Manifest, ManifestEntry, LinkTarget, InboundLink } from './content/manifest.js';
17
+ export { serializeManifest, parseManifest, emptyManifest, verifyManifest, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
18
+ export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink } from './content/manifest.js';
18
19
  export { defineRegistry, emptyValues } from './render/registry.js';
19
20
  export type { ComponentDef, ComponentRegistry, FieldType, AttributeField, SlotKind, SlotDef, ComponentValues, } from './render/registry.js';
20
21
  export { serializeComponent, parseComponent } from './render/component-grammar.js';
@@ -26,38 +27,11 @@ export type { ReferenceOptions } from './render/component-reference.js';
26
27
  export { glyph } from './render/glyph.js';
27
28
  export type { IconSet } from './render/glyph.js';
28
29
  export { remarkDirectiveStamp } from './render/remark-directives.js';
29
- export { rehypeDispatch, isElement, strProp, iconSpan, cardShell, headRow, markFirstList, } from './render/rehype-dispatch.js';
30
+ export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
30
31
  export type { MakeIcon } from './render/rehype-dispatch.js';
31
32
  export { createRenderer } from './render/pipeline.js';
32
33
  export type { RendererOptions } from './render/pipeline.js';
33
34
  export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
34
35
  export { CommitConflictError } from './github/types.js';
35
- export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
36
- export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
37
- export { appCredentials } from './github/credentials.js';
38
- export type { GithubKeyEnv } from './github/credentials.js';
39
36
  export { parseSiteConfig, urlPolicyFrom, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
40
37
  export type { NavNode, SiteConfig } from './nav/site-config.js';
41
- export { permalink } from './content/permalink.js';
42
- export { createContentIndex, fromGlob } from './delivery/content-index.js';
43
- export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem, } from './delivery/content-index.js';
44
- export { createSiteIndex } from './delivery/site-index.js';
45
- export type { SiteIndex, ConceptIndex } from './delivery/site-index.js';
46
- export { createSiteIndexes } from './delivery/site-indexes.js';
47
- export type { SiteIndexes, SiteGlobs } from './delivery/site-indexes.js';
48
- export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
49
- export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
50
- export type { FeedChannel, FeedItem } from './delivery/feeds.js';
51
- export { buildSitemap } from './delivery/sitemap.js';
52
- export type { SitemapUrl } from './delivery/sitemap.js';
53
- export { buildRobots } from './delivery/robots.js';
54
- export { buildSeoMeta } from './delivery/seo.js';
55
- export type { SeoInput, SeoMeta } from './delivery/seo.js';
56
- export { readSeoFields, resolveImageUrl } from './delivery/seo-fields.js';
57
- export type { SeoFields } from './delivery/seo-fields.js';
58
- export { paginate } from './delivery/paginate.js';
59
- export type { Page } from './delivery/paginate.js';
60
- export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './delivery/responses.js';
61
- export { createPublicRoutes } from './sveltekit/public-routes.js';
62
- export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData } from './sveltekit/public-routes.js';
63
- //# sourceMappingURL=index.d.ts.map