@glw907/cairn-cms 0.11.0 → 0.17.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 (124) hide show
  1. package/dist/auth/crypto.d.ts +8 -2
  2. package/dist/auth/crypto.d.ts.map +1 -1
  3. package/dist/auth/crypto.js +12 -2
  4. package/dist/auth/store.d.ts +2 -0
  5. package/dist/auth/store.d.ts.map +1 -1
  6. package/dist/auth/store.js +17 -5
  7. package/dist/components/ComponentForm.svelte +33 -10
  8. package/dist/components/ComponentForm.svelte.d.ts.map +1 -1
  9. package/dist/components/EditPage.svelte +4 -6
  10. package/dist/components/EditPage.svelte.d.ts +1 -1
  11. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  12. package/dist/components/IconPicker.svelte +53 -7
  13. package/dist/components/IconPicker.svelte.d.ts +7 -3
  14. package/dist/components/IconPicker.svelte.d.ts.map +1 -1
  15. package/dist/content/adapter.d.ts +4 -0
  16. package/dist/content/adapter.d.ts.map +1 -0
  17. package/dist/content/adapter.js +4 -0
  18. package/dist/content/concepts.js +2 -2
  19. package/dist/content/schema.d.ts +75 -0
  20. package/dist/content/schema.d.ts.map +1 -0
  21. package/dist/content/schema.js +72 -0
  22. package/dist/content/types.d.ts +30 -7
  23. package/dist/content/types.d.ts.map +1 -1
  24. package/dist/content/validate.d.ts +5 -3
  25. package/dist/content/validate.d.ts.map +1 -1
  26. package/dist/content/validate.js +14 -7
  27. package/dist/delivery/content-index.d.ts +8 -0
  28. package/dist/delivery/content-index.d.ts.map +1 -1
  29. package/dist/delivery/content-index.js +23 -12
  30. package/dist/delivery/feeds.d.ts +1 -1
  31. package/dist/delivery/feeds.d.ts.map +1 -1
  32. package/dist/delivery/feeds.js +31 -16
  33. package/dist/delivery/index.d.ts +5 -1
  34. package/dist/delivery/index.d.ts.map +1 -1
  35. package/dist/delivery/index.js +2 -0
  36. package/dist/delivery/seo-fields.d.ts +22 -0
  37. package/dist/delivery/seo-fields.d.ts.map +1 -0
  38. package/dist/delivery/seo-fields.js +32 -0
  39. package/dist/delivery/site-index.d.ts +2 -2
  40. package/dist/delivery/site-index.d.ts.map +1 -1
  41. package/dist/delivery/site-index.js +16 -18
  42. package/dist/delivery/site-indexes.d.ts +26 -0
  43. package/dist/delivery/site-indexes.d.ts.map +1 -0
  44. package/dist/delivery/site-indexes.js +30 -0
  45. package/dist/env.d.ts.map +1 -1
  46. package/dist/env.js +14 -0
  47. package/dist/github/signing.d.ts +12 -0
  48. package/dist/github/signing.d.ts.map +1 -1
  49. package/dist/github/signing.js +22 -0
  50. package/dist/index.d.ts +9 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -2
  53. package/dist/render/component-grammar.d.ts +7 -0
  54. package/dist/render/component-grammar.d.ts.map +1 -1
  55. package/dist/render/component-grammar.js +27 -8
  56. package/dist/render/component-validate.js +3 -3
  57. package/dist/render/glyph.d.ts +4 -1
  58. package/dist/render/glyph.d.ts.map +1 -1
  59. package/dist/render/glyph.js +6 -2
  60. package/dist/render/pipeline.d.ts +10 -0
  61. package/dist/render/pipeline.d.ts.map +1 -1
  62. package/dist/render/pipeline.js +15 -1
  63. package/dist/render/registry.d.ts +23 -5
  64. package/dist/render/registry.d.ts.map +1 -1
  65. package/dist/render/registry.js +6 -0
  66. package/dist/render/rehype-dispatch.d.ts +1 -5
  67. package/dist/render/rehype-dispatch.d.ts.map +1 -1
  68. package/dist/render/rehype-dispatch.js +71 -19
  69. package/dist/render/remark-directives.d.ts +1 -1
  70. package/dist/render/remark-directives.d.ts.map +1 -1
  71. package/dist/render/remark-directives.js +37 -0
  72. package/dist/render/sanitize-schema.d.ts +20 -0
  73. package/dist/render/sanitize-schema.d.ts.map +1 -0
  74. package/dist/render/sanitize-schema.js +48 -0
  75. package/dist/sveltekit/auth-routes.d.ts.map +1 -1
  76. package/dist/sveltekit/auth-routes.js +29 -11
  77. package/dist/sveltekit/content-routes.js +2 -2
  78. package/dist/sveltekit/guard.d.ts +1 -1
  79. package/dist/sveltekit/guard.d.ts.map +1 -1
  80. package/dist/sveltekit/guard.js +25 -10
  81. package/dist/sveltekit/nav-routes.js +2 -2
  82. package/dist/sveltekit/public-routes.d.ts +3 -0
  83. package/dist/sveltekit/public-routes.d.ts.map +1 -1
  84. package/dist/sveltekit/public-routes.js +10 -3
  85. package/dist/sveltekit/types.d.ts +6 -0
  86. package/dist/sveltekit/types.d.ts.map +1 -1
  87. package/package.json +3 -2
  88. package/src/lib/auth/crypto.ts +14 -2
  89. package/src/lib/auth/store.ts +18 -5
  90. package/src/lib/components/ComponentForm.svelte +33 -10
  91. package/src/lib/components/EditPage.svelte +4 -6
  92. package/src/lib/components/IconPicker.svelte +53 -7
  93. package/src/lib/content/adapter.ts +10 -0
  94. package/src/lib/content/concepts.ts +2 -2
  95. package/src/lib/content/schema.ts +133 -0
  96. package/src/lib/content/types.ts +30 -7
  97. package/src/lib/content/validate.ts +10 -7
  98. package/src/lib/delivery/content-index.ts +32 -12
  99. package/src/lib/delivery/feeds.ts +34 -19
  100. package/src/lib/delivery/index.ts +5 -1
  101. package/src/lib/delivery/seo-fields.ts +43 -0
  102. package/src/lib/delivery/site-index.ts +15 -16
  103. package/src/lib/delivery/site-indexes.ts +64 -0
  104. package/src/lib/env.ts +13 -0
  105. package/src/lib/github/signing.ts +32 -0
  106. package/src/lib/index.ts +8 -2
  107. package/src/lib/render/component-grammar.ts +34 -10
  108. package/src/lib/render/component-validate.ts +3 -3
  109. package/src/lib/render/glyph.ts +6 -2
  110. package/src/lib/render/pipeline.ts +25 -1
  111. package/src/lib/render/registry.ts +27 -5
  112. package/src/lib/render/rehype-dispatch.ts +67 -20
  113. package/src/lib/render/remark-directives.ts +39 -1
  114. package/src/lib/render/sanitize-schema.ts +57 -0
  115. package/src/lib/sveltekit/auth-routes.ts +30 -11
  116. package/src/lib/sveltekit/content-routes.ts +2 -2
  117. package/src/lib/sveltekit/guard.ts +25 -10
  118. package/src/lib/sveltekit/nav-routes.ts +2 -2
  119. package/src/lib/sveltekit/public-routes.ts +13 -3
  120. package/src/lib/sveltekit/types.ts +5 -1
  121. package/dist/render/sanitize.d.ts +0 -8
  122. package/dist/render/sanitize.d.ts.map +0 -1
  123. package/dist/render/sanitize.js +0 -26
  124. package/src/lib/render/sanitize.ts +0 -27
@@ -1,6 +1,7 @@
1
1
  import type { ComponentRegistry } from '../render/registry.js';
2
2
  import type { IconSet } from '../render/glyph.js';
3
3
  import type { DatePrefix } from './ids.js';
4
+ import type { ConceptSchema } from './schema.js';
4
5
  /** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
5
6
  interface FieldBase {
6
7
  /** Frontmatter key and form input name. */
@@ -13,16 +14,37 @@ interface FieldBase {
13
14
  /** A single-line text input. */
14
15
  export interface TextField extends FieldBase {
15
16
  type: 'text';
17
+ /** Minimum character length of a non-empty value. */
18
+ min?: number;
19
+ /** Maximum character length. */
20
+ max?: number;
21
+ /** Exact required character length. */
22
+ length?: number;
23
+ /** A regular-expression source string the value must match. Stored as a string so the field
24
+ * list stays plain serializable data; the validator compiles it. */
25
+ pattern?: string;
16
26
  }
17
27
  /** A multi-line text input. */
18
28
  export interface TextareaField extends FieldBase {
19
29
  type: 'textarea';
20
30
  /** Visible rows; the editor picks a default when omitted. */
21
31
  rows?: number;
32
+ /** Minimum character length of a non-empty value. */
33
+ min?: number;
34
+ /** Maximum character length. */
35
+ max?: number;
36
+ /** Exact required character length. */
37
+ length?: number;
38
+ /** A regular-expression source string the value must match. */
39
+ pattern?: string;
22
40
  }
23
41
  /** A `YYYY-MM-DD` date input. */
24
42
  export interface DateField extends FieldBase {
25
43
  type: 'date';
44
+ /** Earliest allowed date, as `YYYY-MM-DD`. */
45
+ min?: string;
46
+ /** Latest allowed date, as `YYYY-MM-DD`. */
47
+ max?: string;
26
48
  }
27
49
  /** A checkbox; absent means false. */
28
50
  export interface BooleanField extends FieldBase {
@@ -58,18 +80,19 @@ export type ValidationResult = {
58
80
  errors: Record<string, string>;
59
81
  };
60
82
  /**
61
- * Per-site configuration for one content concept (spec §8). Concept-fixed behavior such as
62
- * routability is not here; it lives in the engine's routing table (`CONCEPT_ROUTING`).
83
+ * Per-site configuration for one content concept (spec §8). One `schema`, built with
84
+ * `defineFields`, is the single source of truth for the editor form, the validator, and the
85
+ * inferred frontmatter type. Generic over the schema so a concept's concrete type survives for
86
+ * typed reads. Concept-fixed behavior such as routability is not here; it lives in the engine's
87
+ * routing table (`CONCEPT_ROUTING`).
63
88
  */
64
- export interface ConceptConfig {
89
+ export interface ConceptConfig<S extends ConceptSchema = ConceptSchema> {
65
90
  /** Repo-relative content directory, e.g. "src/content/posts". */
66
91
  dir: string;
67
92
  /** Sidebar label; defaults from the concept id when omitted. */
68
93
  label?: string;
69
- /** Drives the per-concept frontmatter form, in order. */
70
- fields: FrontmatterField[];
71
- /** Validate submitted frontmatter before any commit. */
72
- validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
94
+ /** The concept's schema: the form projection, the generated validator, and the inferred type. */
95
+ schema: S;
73
96
  }
74
97
  /**
75
98
  * A concept's URL policy, set per concept in the YAML site-config (not the adapter). `permalink` is
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  import type { FrontmatterField, ValidationResult } from './types.js';
2
2
  /**
3
3
  * Validate raw frontmatter against a field list. Required text and date fields must be
4
- * non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
5
- * and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
6
- * any required field is empty.
4
+ * non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
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
7
+ * carries only meaningful values and committed frontmatter stays minimal. Returns the
8
+ * normalized data, or field-keyed errors when any required field is empty.
7
9
  *
8
10
  * Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
9
11
  * where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/lib/content/validate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,gBAAgB,CA8BlB"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/lib/content/validate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,gBAAgB,CA+BlB"}
@@ -1,9 +1,11 @@
1
1
  import { dateInputValue } from './frontmatter.js';
2
2
  /**
3
3
  * Validate raw frontmatter against a field list. Required text and date fields must be
4
- * non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
5
- * and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
6
- * any required field is empty.
4
+ * non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
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
7
+ * carries only meaningful values and committed frontmatter stays minimal. Returns the
8
+ * normalized data, or field-keyed errors when any required field is empty.
7
9
  *
8
10
  * Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
9
11
  * where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
@@ -16,28 +18,33 @@ export function validateFields(fields, frontmatter) {
16
18
  const value = frontmatter[field.name];
17
19
  switch (field.type) {
18
20
  case 'boolean':
19
- data[field.name] = value === true;
21
+ // Absent or unchecked means false; omit it so a published file carries no draft: false noise.
22
+ if (value === true)
23
+ data[field.name] = true;
20
24
  break;
21
25
  case 'tags':
22
26
  case 'freetags': {
23
27
  const list = Array.isArray(value) ? value.map(String) : [];
24
28
  if (field.required && list.length === 0)
25
29
  errors[field.name] = `${field.label} is required`;
26
- data[field.name] = list;
30
+ if (list.length > 0)
31
+ data[field.name] = list;
27
32
  break;
28
33
  }
29
34
  case 'date': {
30
35
  const text = value instanceof Date ? dateInputValue(value) : typeof value === 'string' ? value.trim() : '';
31
36
  if (field.required && text === '')
32
37
  errors[field.name] = `${field.label} is required`;
33
- data[field.name] = text;
38
+ if (text !== '')
39
+ data[field.name] = text;
34
40
  break;
35
41
  }
36
42
  default: {
37
43
  const text = typeof value === 'string' ? value.trim() : '';
38
44
  if (field.required && text === '')
39
45
  errors[field.name] = `${field.label} is required`;
40
- data[field.name] = text;
46
+ if (text !== '')
47
+ data[field.name] = text;
41
48
  }
42
49
  }
43
50
  }
@@ -24,6 +24,12 @@ export interface ContentEntry<F = Record<string, unknown>> extends ContentSummar
24
24
  frontmatter: F;
25
25
  body: string;
26
26
  }
27
+ /** One entry's validation failure, recorded at build for the site aggregator's gate. */
28
+ export interface ContentProblem {
29
+ id: string;
30
+ draft: boolean;
31
+ errors: Record<string, string>;
32
+ }
27
33
  /** The per-concept query surface. */
28
34
  export interface ContentIndex<F = Record<string, unknown>> {
29
35
  all(opts?: {
@@ -41,6 +47,8 @@ export interface ContentIndex<F = Record<string, unknown>> {
41
47
  newer?: ContentSummary;
42
48
  older?: ContentSummary;
43
49
  };
50
+ /** Per-entry validation failures recorded at build, for the site-level build gate. */
51
+ problems(): ContentProblem[];
44
52
  }
45
53
  /** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
46
54
  export declare function fromGlob(record: Record<string, string>): RawFile[];
@@ -1 +1 @@
1
- {"version":3,"file":"content-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/content-index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;wEAEwE;AACxE,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,cAAc;IAC/E,WAAW,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,GAAG,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IAC1D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IACzE,OAAO,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;CAC1E;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAElE;AAqBD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,iBAAiB,GAC5B,YAAY,CAAC,CAAC,CAAC,CA2DjB"}
1
+ {"version":3,"file":"content-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/content-index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;wEAEwE;AACxE,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,cAAc;IAC/E,WAAW,EAAE,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,GAAG,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IAC1D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IACzE,OAAO,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACzE,sFAAsF;IACtF,QAAQ,IAAI,cAAc,EAAE,CAAC;CAC9B;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAElE;AAqBD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,iBAAiB,GAC5B,YAAY,CAAC,CAAC,CAAC,CAsEjB"}
@@ -29,26 +29,36 @@ function asTags(value) {
29
29
  }
30
30
  /** Build a concept's index from its raw files and normalized descriptor. */
31
31
  export function createContentIndex(files, descriptor) {
32
- const entries = files.map((file) => {
32
+ const problems = [];
33
+ const entries = [];
34
+ for (const file of files) {
33
35
  const id = idFromFilename(basename(file.path));
34
36
  const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
35
- const { frontmatter, body } = parseMarkdown(file.raw);
36
- const date = asDate(frontmatter.date);
37
- return {
37
+ const { frontmatter: raw, body } = parseMarkdown(file.raw);
38
+ const date = asDate(raw.date);
39
+ 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.
42
+ const result = descriptor.validate(raw, body);
43
+ if (!result.ok) {
44
+ problems.push({ id, draft, errors: result.errors });
45
+ continue;
46
+ }
47
+ entries.push({
38
48
  id,
39
49
  slug,
40
50
  permalink: permalink(descriptor, { id, slug, date }),
41
- title: asString(frontmatter.title) ?? id,
51
+ title: asString(raw.title) ?? id,
42
52
  date,
43
- updated: asDate(frontmatter.updated),
44
- tags: asTags(frontmatter.tags),
45
- excerpt: deriveExcerpt(body, { description: asString(frontmatter.description) }),
53
+ updated: asDate(raw.updated),
54
+ tags: asTags(raw.tags),
55
+ excerpt: deriveExcerpt(body, { description: asString(raw.description) }),
46
56
  wordCount: wordCount(body),
47
- draft: frontmatter.draft === true,
48
- frontmatter: frontmatter,
57
+ draft,
58
+ frontmatter: result.data,
49
59
  body,
50
- };
51
- });
60
+ });
61
+ }
52
62
  // Dated concepts sort newest-first; undated concepts (Pages) sort by title.
53
63
  const sorted = [...entries].sort((a, b) => descriptor.routing.dated ? (b.date ?? '').localeCompare(a.date ?? '') : a.title.localeCompare(b.title));
54
64
  const summarize = (entry) => {
@@ -82,5 +92,6 @@ export function createContentIndex(files, descriptor) {
82
92
  older: i < list.length - 1 ? summarize(list[i + 1]) : undefined,
83
93
  };
84
94
  },
95
+ problems: () => problems,
85
96
  };
86
97
  }
@@ -14,7 +14,7 @@ export interface FeedChannel {
14
14
  export interface FeedItem {
15
15
  title: string;
16
16
  url: string;
17
- date: string;
17
+ date?: string;
18
18
  updated?: string;
19
19
  summary: string;
20
20
  contentHtml?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/feeds.ts"],"names":[],"mappings":"AAKA,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAyBD,iCAAiC;AACjC,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAkC5E;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAwB7E"}
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/feeds.ts"],"names":[],"mappings":"AAKA,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAiCD,iCAAiC;AACjC,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAqC5E;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CA4B7E"}
@@ -13,30 +13,41 @@ function escapeXml(value) {
13
13
  function cdataSafe(value) {
14
14
  return value.replace(/]]>/g, ']]]]><![CDATA[>');
15
15
  }
16
- /** Format a YYYY-MM-DD (or ISO) string as an RFC-822 date in UTC, as RSS wants. */
16
+ /** Parse a YYYY-MM-DD (or ISO) string as a UTC instant. Returns undefined for an absent or
17
+ * unparseable date, so a feed omits the date field rather than emit Invalid Date or throw. */
18
+ function parseFeedDate(date) {
19
+ if (!date)
20
+ return undefined;
21
+ const at = new Date(`${date.slice(0, 10)}T00:00:00.000Z`);
22
+ return Number.isNaN(at.getTime()) ? undefined : at;
23
+ }
24
+ /** Format a date as an RFC-822 string in UTC, as RSS wants, or undefined when it cannot parse. */
17
25
  function rfc822(date) {
18
- return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toUTCString();
26
+ return parseFeedDate(date)?.toUTCString();
19
27
  }
20
- /** Format a YYYY-MM-DD (or ISO) string as an ISO-8601 instant in UTC. */
28
+ /** Format a date as an ISO-8601 instant in UTC, or undefined when it cannot parse. */
21
29
  function iso(date) {
22
- return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toISOString();
30
+ return parseFeedDate(date)?.toISOString();
23
31
  }
24
32
  /** Build an RSS 2.0 document. */
25
33
  export function buildRssFeed(channel, items) {
26
34
  const entries = items
27
35
  .map((item) => {
28
36
  const content = item.contentHtml ?? item.summary;
37
+ const pubDate = rfc822(item.date);
29
38
  return [
30
39
  ' <item>',
31
40
  ` <title>${escapeXml(item.title)}</title>`,
32
41
  ` <link>${escapeXml(item.url)}</link>`,
33
42
  ` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
34
- ` <pubDate>${rfc822(item.date)}</pubDate>`,
43
+ pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
35
44
  ` <description>${escapeXml(item.summary)}</description>`,
36
45
  // CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
37
46
  ` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
38
47
  ' </item>',
39
- ].join('\n');
48
+ ]
49
+ .filter((line) => line !== '')
50
+ .join('\n');
40
51
  })
41
52
  .join('\n');
42
53
  return [
@@ -66,15 +77,19 @@ export function buildJsonFeed(channel, items) {
66
77
  feed_url: channel.feedUrl,
67
78
  ...(channel.language ? { language: channel.language } : {}),
68
79
  ...(channel.author ? { authors: [channel.author] } : {}),
69
- items: items.map((item) => ({
70
- id: item.url,
71
- url: item.url,
72
- title: item.title,
73
- summary: item.summary,
74
- date_published: iso(item.date),
75
- ...(item.updated ? { date_modified: iso(item.updated) } : {}),
76
- ...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
77
- ...(item.tags && item.tags.length ? { tags: item.tags } : {}),
78
- })),
80
+ items: items.map((item) => {
81
+ const datePublished = iso(item.date);
82
+ const dateModified = iso(item.updated);
83
+ return {
84
+ id: item.url,
85
+ url: item.url,
86
+ title: item.title,
87
+ summary: item.summary,
88
+ ...(datePublished ? { date_published: datePublished } : {}),
89
+ ...(dateModified ? { date_modified: dateModified } : {}),
90
+ ...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
91
+ ...(item.tags && item.tags.length ? { tags: item.tags } : {}),
92
+ };
93
+ }),
79
94
  }, null, 2);
80
95
  }
@@ -1,7 +1,9 @@
1
1
  export { createContentIndex, fromGlob } from './content-index.js';
2
- export type { RawFile, ContentSummary, ContentEntry, ContentIndex } from './content-index.js';
2
+ export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
3
3
  export { createSiteIndex } from './site-index.js';
4
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';
5
7
  export { siteDescriptors } from './site-descriptors.js';
6
8
  export { deriveExcerpt, wordCount } from './excerpt.js';
7
9
  export { buildRssFeed, buildJsonFeed } from './feeds.js';
@@ -11,6 +13,8 @@ export type { SitemapUrl } from './sitemap.js';
11
13
  export { buildRobots } from './robots.js';
12
14
  export { buildSeoMeta } from './seo.js';
13
15
  export type { SeoInput, SeoMeta } from './seo.js';
16
+ export { readSeoFields, resolveImageUrl } from './seo-fields.js';
17
+ export type { SeoFields } from './seo-fields.js';
14
18
  export { paginate } from './paginate.js';
15
19
  export type { Page } from './paginate.js';
16
20
  export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9G,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
@@ -5,12 +5,14 @@
5
5
  // github, or email, so importing it does not pull the server backend into a public bundle.
6
6
  export { createContentIndex, fromGlob } from './content-index.js';
7
7
  export { createSiteIndex } from './site-index.js';
8
+ export { createSiteIndexes } from './site-indexes.js';
8
9
  export { siteDescriptors } from './site-descriptors.js';
9
10
  export { deriveExcerpt, wordCount } from './excerpt.js';
10
11
  export { buildRssFeed, buildJsonFeed } from './feeds.js';
11
12
  export { buildSitemap } from './sitemap.js';
12
13
  export { buildRobots } from './robots.js';
13
14
  export { buildSeoMeta } from './seo.js';
15
+ export { readSeoFields, resolveImageUrl } from './seo-fields.js';
14
16
  export { paginate } from './paginate.js';
15
17
  export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
16
18
  export { jsonLdScript } from './json-ld.js';
@@ -0,0 +1,22 @@
1
+ /** The head fields a concept can carry in frontmatter. Each is optional and omitted when absent.
2
+ * `author` is article-scoped downstream: the head builder emits `article:author` only for a dated
3
+ * entry, so an `author` on an undated Page is read here but not rendered. */
4
+ export interface SeoFields {
5
+ description?: string;
6
+ image?: string;
7
+ robots?: string;
8
+ author?: string;
9
+ }
10
+ /** Read the known SEO head fields off an entry's normalized frontmatter. Keeps a present string,
11
+ * trimmed, and omits an absent, empty, or non-string value. Trimming the stored value keeps a stray
12
+ * `robots: " noindex "` from reaching the head tag with surrounding whitespace. The field must be
13
+ * declared in the concept's schema to survive the validate-once read; an undeclared key is not on the
14
+ * normalized frontmatter. */
15
+ export declare function readSeoFields(frontmatter: Record<string, unknown>): SeoFields;
16
+ /** Resolve an author-supplied image path to an absolute URL against the site origin. An absolute or
17
+ * protocol-relative URL passes through; a root-relative path anchors to the origin; a malformed
18
+ * string returns undefined rather than throwing at build. The sites use a bare-domain origin, so a
19
+ * bare path also anchors to the origin root; against a sub-path origin it would resolve relative to
20
+ * that path, per the WHATWG URL rules. */
21
+ export declare function resolveImageUrl(image: string, origin: string): string | undefined;
22
+ //# sourceMappingURL=seo-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-fields.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/seo-fields.ts"],"names":[],"mappings":"AAKA;;8EAE8E;AAC9E,MAAM,WAAW,SAAS;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;8BAI8B;AAC9B,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAO7E;AAED;;;;2CAI2C;AAC3C,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjF"}
@@ -0,0 +1,32 @@
1
+ // cairn-cms: the SEO head fields read at the cross-concept boundary (schema-source-of-truth design,
2
+ // Plan 3). The catch-all route resolves any concept by request path, so the entry's frontmatter is
3
+ // typed Record<string, unknown>; this reads the known head fields by name and coerces. Kept apart
4
+ // from seo.ts (the head builder) so reading frontmatter and building the head stay distinct concerns.
5
+ const KEYS = ['description', 'image', 'robots', 'author'];
6
+ /** Read the known SEO head fields off an entry's normalized frontmatter. Keeps a present string,
7
+ * trimmed, and omits an absent, empty, or non-string value. Trimming the stored value keeps a stray
8
+ * `robots: " noindex "` from reaching the head tag with surrounding whitespace. The field must be
9
+ * declared in the concept's schema to survive the validate-once read; an undeclared key is not on the
10
+ * normalized frontmatter. */
11
+ export function readSeoFields(frontmatter) {
12
+ const fields = {};
13
+ for (const key of KEYS) {
14
+ const value = frontmatter[key];
15
+ if (typeof value === 'string' && value.trim() !== '')
16
+ fields[key] = value.trim();
17
+ }
18
+ return fields;
19
+ }
20
+ /** Resolve an author-supplied image path to an absolute URL against the site origin. An absolute or
21
+ * protocol-relative URL passes through; a root-relative path anchors to the origin; a malformed
22
+ * string returns undefined rather than throwing at build. The sites use a bare-domain origin, so a
23
+ * bare path also anchors to the origin root; against a sub-path origin it would resolve relative to
24
+ * that path, per the WHATWG URL rules. */
25
+ export function resolveImageUrl(image, origin) {
26
+ try {
27
+ return new URL(image, origin).href;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -25,8 +25,8 @@ export interface SiteIndex {
25
25
  }
26
26
  /**
27
27
  * Union per-concept indexes into a site-level resolver. Throws on a duplicate permalink and,
28
- * unless `validate` is `false`, on any entry whose frontmatter fails its concept's validator,
29
- * so malformed content fails the build instead of shipping.
28
+ * unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
29
+ * validator, so malformed content fails the build instead of shipping.
30
30
  */
31
31
  export declare function createSiteIndex(concepts: ConceptIndex[], opts?: {
32
32
  validate?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"site-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErF,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACpF,uGAAuG;IACvG,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC9C,0EAA0E;IAC1E,GAAG,IAAI,cAAc,EAAE,CAAC;CACzB;AA2BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,SAAS,CAmCtG"}
1
+ {"version":3,"file":"site-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErF,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACpF,uGAAuG;IACvG,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC9C,0EAA0E;IAC1E,GAAG,IAAI,cAAc,EAAE,CAAC;CACzB;AAqBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,SAAS,CAwCtG"}
@@ -2,34 +2,32 @@
2
2
  function normalizePath(path) {
3
3
  return path.length > 1 ? path.replace(/\/+$/, '') : path;
4
4
  }
5
- /** Validate every entry (drafts included) against its concept, aggregating failures. */
6
- function validateAll(concepts) {
5
+ /** Collect non-draft validation failures across concepts from each index's recorded verdicts. */
6
+ function siteProblems(concepts) {
7
7
  const problems = [];
8
8
  for (const { descriptor, index } of concepts) {
9
- for (const summary of index.all({ includeDrafts: true })) {
10
- const entry = index.byId(summary.id);
11
- if (!entry)
12
- continue;
13
- const result = descriptor.validate(entry.frontmatter, entry.body);
14
- if (!result.ok) {
15
- for (const [field, message] of Object.entries(result.errors)) {
16
- problems.push(`${descriptor.dir}/${summary.id}: ${field}: ${message}`);
17
- }
9
+ for (const problem of index.problems()) {
10
+ if (problem.draft)
11
+ continue; // a half-finished draft never ships, so it does not fail the build
12
+ for (const [field, message] of Object.entries(problem.errors)) {
13
+ problems.push(`${descriptor.dir}/${problem.id}: ${field}: ${message}`);
18
14
  }
19
15
  }
20
16
  }
21
- if (problems.length > 0) {
22
- throw new Error(`site index: ${problems.length} invalid frontmatter field(s):\n ${problems.join('\n ')}`);
23
- }
17
+ return problems;
24
18
  }
25
19
  /**
26
20
  * Union per-concept indexes into a site-level resolver. Throws on a duplicate permalink and,
27
- * unless `validate` is `false`, on any entry whose frontmatter fails its concept's validator,
28
- * so malformed content fails the build instead of shipping.
21
+ * unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
22
+ * validator, so malformed content fails the build instead of shipping.
29
23
  */
30
24
  export function createSiteIndex(concepts, opts = {}) {
31
- if (opts.validate !== false)
32
- validateAll(concepts);
25
+ if (opts.validate !== false) {
26
+ const problems = siteProblems(concepts);
27
+ if (problems.length > 0) {
28
+ throw new Error(`site index: ${problems.length} invalid frontmatter field(s):\n ${problems.join('\n ')}`);
29
+ }
30
+ }
33
31
  const byPath = new Map();
34
32
  const byId = new Map();
35
33
  for (const { descriptor, index } of concepts) {
@@ -0,0 +1,26 @@
1
+ import type { CairnAdapter, ConceptConfig } from '../content/types.js';
2
+ import type { Infer } from '../content/schema.js';
3
+ import type { SiteConfig } from '../nav/site-config.js';
4
+ import type { ContentIndex } from './content-index.js';
5
+ import type { SiteIndex } from './site-index.js';
6
+ /** A per-concept raw glob record (`{ path: raw }`) keyed by concept id, from `import.meta.glob`. */
7
+ export type SiteGlobs<A extends CairnAdapter> = {
8
+ [K in keyof A['content']]?: Record<string, string>;
9
+ };
10
+ /** The typed per-concept indexes plus the cross-concept `site` resolver. A concept literally named
11
+ * `site` is not supported, since `site` is the reserved resolver key. */
12
+ export type SiteIndexes<A extends CairnAdapter> = {
13
+ [K in keyof A['content']]: ContentIndex<NonNullable<A['content'][K]> extends ConceptConfig<infer S> ? Infer<S> : Record<string, unknown>>;
14
+ } & {
15
+ readonly site: SiteIndex;
16
+ };
17
+ /**
18
+ * Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
19
+ * globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
20
+ * glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
21
+ * of the build gate, exactly as on `createSiteIndex`.
22
+ */
23
+ export declare function createSiteIndexes<const A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>, opts?: {
24
+ validate?: boolean;
25
+ }): SiteIndexes<A>;
26
+ //# sourceMappingURL=site-indexes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-indexes.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-indexes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/D,oGAAoG;AACpG,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,YAAY,IAAI;KAC7C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CACnD,CAAC;AAEF;0EAC0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CACrC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACjG;CACF,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC5D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,WAAW,CAAC,CAAC,CAAC,CAwBhB"}
@@ -0,0 +1,30 @@
1
+ import { siteDescriptors } from './site-descriptors.js';
2
+ import { createContentIndex, fromGlob } from './content-index.js';
3
+ import { createSiteIndex } from './site-index.js';
4
+ /**
5
+ * Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
6
+ * globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
7
+ * glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
8
+ * of the build gate, exactly as on `createSiteIndex`.
9
+ */
10
+ export function createSiteIndexes(adapter, config, globs, opts = {}) {
11
+ const descriptors = siteDescriptors(adapter, config);
12
+ const globRecord = globs;
13
+ const byConcept = {};
14
+ const conceptIndexes = [];
15
+ for (const descriptor of descriptors) {
16
+ if (descriptor.id === 'site') {
17
+ throw new Error('createSiteIndexes: a concept cannot be named "site", which is the reserved cross-concept resolver key');
18
+ }
19
+ if (!Object.prototype.hasOwnProperty.call(globRecord, descriptor.id)) {
20
+ const passed = Object.keys(globRecord);
21
+ throw new Error(`createSiteIndexes: no glob passed for concept "${descriptor.id}"; pass its import.meta.glob (an empty {} for an intentionally empty concept). Globs passed: ${passed.length ? passed.join(', ') : '(none)'}`);
22
+ }
23
+ const record = globRecord[descriptor.id] ?? {};
24
+ const index = createContentIndex(fromGlob(record), descriptor);
25
+ byConcept[descriptor.id] = index;
26
+ conceptIndexes.push({ descriptor, index });
27
+ }
28
+ const site = createSiteIndex(conceptIndexes, opts);
29
+ return { ...byConcept, site };
30
+ }
package/dist/env.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmBrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}