@glw907/cairn-cms 0.68.0 → 0.76.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.
- package/CHANGELOG.md +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +2 -5
- package/dist/delivery/public-routes.js +15 -1
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.js +7 -8
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +5 -1
- package/src/lib/ambient.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +9 -8
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +17 -3
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
package/src/lib/content/types.ts
CHANGED
|
@@ -9,107 +9,14 @@
|
|
|
9
9
|
// boundary to the editor form.
|
|
10
10
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
11
11
|
import type { IconSet } from '../render/glyph.js';
|
|
12
|
+
import type { IslandRegistry } from '../islands/types.js';
|
|
13
|
+
import type { BackendProvider } from '../github/backend.js';
|
|
12
14
|
import type { DatePrefix } from './ids.js';
|
|
13
|
-
import type {
|
|
15
|
+
import type { Fieldset } from './fieldset.js';
|
|
16
|
+
import type { FieldDescriptor } from './fields.js';
|
|
14
17
|
import type { LinkResolve } from './links.js';
|
|
15
18
|
import type { VariantSpec } from '../media/transform-url.js';
|
|
16
19
|
|
|
17
|
-
/** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
|
|
18
|
-
interface FieldBase {
|
|
19
|
-
/** Frontmatter key and form input name. */
|
|
20
|
-
name: string;
|
|
21
|
-
/** Form label. */
|
|
22
|
-
label: string;
|
|
23
|
-
/** A required field fails validation when empty (spec §7.4). */
|
|
24
|
-
required?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* One author-facing sentence shown under the field in the editor, in plain end-user language.
|
|
27
|
-
* Optional; render nothing when absent. Not a validation rule.
|
|
28
|
-
*/
|
|
29
|
-
description?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A single-line text input. */
|
|
33
|
-
export interface TextField extends FieldBase {
|
|
34
|
-
type: 'text';
|
|
35
|
-
/** Minimum character length of a non-empty value. */
|
|
36
|
-
min?: number;
|
|
37
|
-
/** Maximum character length. */
|
|
38
|
-
max?: number;
|
|
39
|
-
/** Exact required character length. */
|
|
40
|
-
length?: number;
|
|
41
|
-
/**
|
|
42
|
-
* A regular-expression source string the value must match. Stored as a string so the field
|
|
43
|
-
* list stays plain serializable data; the validator compiles it.
|
|
44
|
-
*/
|
|
45
|
-
pattern?: string;
|
|
46
|
-
}
|
|
47
|
-
/** A multi-line text input. */
|
|
48
|
-
export interface TextareaField extends FieldBase {
|
|
49
|
-
type: 'textarea';
|
|
50
|
-
/** Visible rows; the editor picks a default when omitted. */
|
|
51
|
-
rows?: number;
|
|
52
|
-
/** Minimum character length of a non-empty value. */
|
|
53
|
-
min?: number;
|
|
54
|
-
/** Maximum character length. */
|
|
55
|
-
max?: number;
|
|
56
|
-
/** Exact required character length. */
|
|
57
|
-
length?: number;
|
|
58
|
-
/** A regular-expression source string the value must match. */
|
|
59
|
-
pattern?: string;
|
|
60
|
-
}
|
|
61
|
-
/** A `YYYY-MM-DD` date input. */
|
|
62
|
-
export interface DateField extends FieldBase {
|
|
63
|
-
type: 'date';
|
|
64
|
-
/** Earliest allowed date, as `YYYY-MM-DD`. */
|
|
65
|
-
min?: string;
|
|
66
|
-
/** Latest allowed date, as `YYYY-MM-DD`. */
|
|
67
|
-
max?: string;
|
|
68
|
-
}
|
|
69
|
-
/** A checkbox; absent means false. */
|
|
70
|
-
export interface BooleanField extends FieldBase {
|
|
71
|
-
type: 'boolean';
|
|
72
|
-
}
|
|
73
|
-
/** A closed-vocabulary tag set, rendered as checkboxes (ecnordic). */
|
|
74
|
-
export interface TagsField extends FieldBase {
|
|
75
|
-
type: 'tags';
|
|
76
|
-
/** The controlled vocabulary. */
|
|
77
|
-
options: readonly string[];
|
|
78
|
-
}
|
|
79
|
-
/** Free-form tags, edited as one comma-separated input (907). */
|
|
80
|
-
export interface FreeTagsField extends FieldBase {
|
|
81
|
-
type: 'freetags';
|
|
82
|
-
placeholder?: string;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* A hero image set in frontmatter. The stored value is the nested object
|
|
86
|
-
* `{ src: string; alt: string; caption?: string }`, where `src` is a 2b `media:` reference, `alt`
|
|
87
|
-
* is the screen-reader description, and `caption` is an optional line the site template may show.
|
|
88
|
-
* One image serves two jobs: the template's lead image and the social-card image. The field feeding
|
|
89
|
-
* the social card is the `seo`-flagged one, defaulting to the field named `image`; a concept declares
|
|
90
|
-
* at most one SEO image field.
|
|
91
|
-
*/
|
|
92
|
-
export interface ImageField extends FieldBase {
|
|
93
|
-
type: 'image';
|
|
94
|
-
/** Whether this field feeds the social-card image. The field named `image` defaults to true. */
|
|
95
|
-
seo?: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The discriminated union the per-concept frontmatter form is generated from. A scalar field type
|
|
100
|
-
* is one variant here plus one decode arm in `frontmatterFromForm` and one in `validateFields`. The
|
|
101
|
-
* structured `image` field additionally needs a read-back arm in `formValues` and a type-inference
|
|
102
|
-
* arm in `schema.ts`, since its value is a nested object rather than a single string.
|
|
103
|
-
*/
|
|
104
|
-
export type FrontmatterField =
|
|
105
|
-
| TextField
|
|
106
|
-
| TextareaField
|
|
107
|
-
| DateField
|
|
108
|
-
| BooleanField
|
|
109
|
-
| TagsField
|
|
110
|
-
| FreeTagsField
|
|
111
|
-
| ImageField;
|
|
112
|
-
|
|
113
20
|
/**
|
|
114
21
|
* The stored value of an `image` field: a `media:` reference, a screen-reader description, and an
|
|
115
22
|
* optional caption.
|
|
@@ -122,59 +29,75 @@ export interface ImageValue {
|
|
|
122
29
|
decorative?: boolean;
|
|
123
30
|
}
|
|
124
31
|
|
|
32
|
+
/** One validation failure located by a path: a top-level key, then a row index and/or a leaf sub-key. */
|
|
33
|
+
export interface ValidationIssue {
|
|
34
|
+
/** The path to the failing field, e.g. ['faq', 0, 'question'] or ['address', 'city'] or ['title']. */
|
|
35
|
+
path: (string | number)[];
|
|
36
|
+
/** The author-facing message, naming the field's label. */
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
125
40
|
/**
|
|
126
|
-
* A validator's verdict. On success it carries the normalized frontmatter to commit; on
|
|
127
|
-
*
|
|
128
|
-
*
|
|
41
|
+
* A validator's verdict. On success it carries the normalized frontmatter to commit; on failure it
|
|
42
|
+
* carries field-keyed error messages (the empty key is a form-level error) and, additively, the
|
|
43
|
+
* located `issues` with multi-segment paths so the form can route a nested-container error to the
|
|
44
|
+
* right input. Invalid input bounces to the form and never reaches git (spec §7.4).
|
|
129
45
|
*/
|
|
130
46
|
export type ValidationResult =
|
|
131
47
|
| { ok: true; data: Record<string, unknown> }
|
|
132
|
-
| { ok: false; errors: Record<string, string
|
|
48
|
+
| { ok: false; errors: Record<string, string>; issues?: ValidationIssue[] };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A field descriptor with its frontmatter key re-attached as `name`. This is the normalized form
|
|
52
|
+
* `ConceptDescriptor.fields` carries: `normalizeConcepts` derives it from a concept's `fieldset`
|
|
53
|
+
* record so every consumer (the editor form, the form decoder, the media extractor) iterates an
|
|
54
|
+
* array and reads `name` rather than the keyed record.
|
|
55
|
+
*/
|
|
56
|
+
export type NamedField = FieldDescriptor & { name: string };
|
|
133
57
|
|
|
134
58
|
/**
|
|
135
|
-
* Per-site configuration for one content concept (spec §8). One `
|
|
136
|
-
* `
|
|
137
|
-
* inferred frontmatter type. Generic over the
|
|
138
|
-
* typed reads.
|
|
139
|
-
*
|
|
59
|
+
* Per-site configuration for one content concept (spec §8). One `fields` fieldset, built with
|
|
60
|
+
* `fieldset`, is the single source of truth for the editor form, the validator, and the
|
|
61
|
+
* inferred frontmatter type. Generic over the fieldset so a concept's concrete type survives for
|
|
62
|
+
* typed reads. A concept also declares its own routing and URL policy here (`routing`, `permalink`,
|
|
63
|
+
* `datePrefix`), resolved by `normalizeConcepts`.
|
|
140
64
|
*/
|
|
141
|
-
export interface ConceptConfig<S extends
|
|
65
|
+
export interface ConceptConfig<S extends Fieldset = Fieldset> {
|
|
142
66
|
/** Repo-relative content directory, e.g. "src/content/posts". */
|
|
143
67
|
dir: string;
|
|
144
68
|
/** Sidebar label; defaults from the concept id when omitted. */
|
|
145
69
|
label?: string;
|
|
146
70
|
/** The singular noun for the create affordances ("New post"); defaults to `label` when omitted. */
|
|
147
71
|
singular?: string;
|
|
148
|
-
/** The concept's
|
|
149
|
-
|
|
72
|
+
/** The concept's fieldset: the form projection, the generated validator, and the inferred type. */
|
|
73
|
+
fields: S;
|
|
74
|
+
/**
|
|
75
|
+
* This concept's routing. A named shorthand (`'feed'` dated and in feeds, `'page'` a routable
|
|
76
|
+
* static page, `'embedded'` not routable) or an explicit rule. Omitted means `'page'`.
|
|
77
|
+
*/
|
|
78
|
+
routing?: 'feed' | 'page' | 'embedded' | RoutingRule;
|
|
79
|
+
/** The permalink pattern, root-relative, e.g. `/blog/:year/:slug`. Defaults by concept id. */
|
|
80
|
+
permalink?: string;
|
|
81
|
+
/** Date-prefix granularity for a dated concept's id-to-slug stripping. Defaults to `day`. */
|
|
82
|
+
datePrefix?: DatePrefix;
|
|
150
83
|
/**
|
|
151
84
|
* Frontmatter keys to surface on each `ContentSummary.fields`, so a list card reads an authored
|
|
152
|
-
* field without a per-entry detail read. Each key should also be declared in `
|
|
85
|
+
* field without a per-entry detail read. Each key should also be declared in `fields`.
|
|
153
86
|
*/
|
|
154
87
|
summaryFields?: string[];
|
|
155
88
|
}
|
|
156
89
|
|
|
157
90
|
/**
|
|
158
|
-
* A concept's URL policy,
|
|
159
|
-
* a `/`-prefixed pattern of literal
|
|
160
|
-
*
|
|
161
|
-
* `normalizeConcepts` when omitted.
|
|
91
|
+
* A concept's URL policy, declared on the adapter concept itself (`ConceptConfig.permalink` and
|
|
92
|
+
* `ConceptConfig.datePrefix`) since Contract v2. `permalink` is a `/`-prefixed pattern of literal
|
|
93
|
+
* segments and the tokens `:slug`, `:year`, `:month`, `:day`. `datePrefix` is the filename
|
|
94
|
+
* date-prefix granularity for a dated concept. Both default in `normalizeConcepts` when omitted.
|
|
162
95
|
*/
|
|
163
96
|
export interface ConceptUrlPolicy {
|
|
164
97
|
permalink?: string;
|
|
165
98
|
datePrefix?: DatePrefix;
|
|
166
99
|
}
|
|
167
100
|
|
|
168
|
-
/** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
|
|
169
|
-
export interface BackendConfig {
|
|
170
|
-
owner: string;
|
|
171
|
-
repo: string;
|
|
172
|
-
/** Commit target, e.g. "main". */
|
|
173
|
-
branch: string;
|
|
174
|
-
appId: string;
|
|
175
|
-
installationId: string;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
101
|
/** Magic-link sender identity for Cloudflare Email Sending. */
|
|
179
102
|
export interface SenderConfig {
|
|
180
103
|
from: string;
|
|
@@ -256,67 +179,73 @@ export interface AssetConfig {
|
|
|
256
179
|
transformations?: boolean;
|
|
257
180
|
}
|
|
258
181
|
|
|
259
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* The site's one renderer (design decision 4): the editor preview and every public page call it.
|
|
184
|
+
* Entry-aware so a custom renderer can vary output by concept or frontmatter; the default reads only
|
|
185
|
+
* `body` plus the resolvers. `resolve` rewrites cairn: links to live permalinks (the build passes a
|
|
186
|
+
* site-resolver-backed resolver, the preview a manifest-backed one); `resolveMedia` resolves media:
|
|
187
|
+
* references the same way. `concept` and `frontmatter` carry the entry's context for an entry render
|
|
188
|
+
* and are absent for the standalone component-insert preview.
|
|
189
|
+
*/
|
|
190
|
+
export type SiteRender = (input: {
|
|
191
|
+
body: string;
|
|
192
|
+
concept?: string;
|
|
193
|
+
frontmatter?: Record<string, unknown>;
|
|
194
|
+
resolve?: LinkResolve;
|
|
195
|
+
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
196
|
+
}) => Promise<string>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The single seam the engine consumes. A site implements this at `src/lib/cairn.config.ts`, in six
|
|
200
|
+
* subsystem groups (spec §8): the content concepts, the commit backend, the magic-link sender, the
|
|
201
|
+
* render subsystem, the optional media stack, and the admin-experience knobs. The internal manifest
|
|
202
|
+
* and dictionary paths are not here; `composeRuntime` defaults them by convention.
|
|
203
|
+
*/
|
|
260
204
|
export interface CairnAdapter {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
205
|
+
/** The site's concepts, keyed by id. Posts and pages are the documented defaults; a site may add more. */
|
|
206
|
+
content: Record<string, ConceptConfig>;
|
|
207
|
+
/** The commit backend provider, from `githubApp({ ... })` (the GitHub App today). */
|
|
208
|
+
backend: BackendProvider;
|
|
209
|
+
/** The magic-link sender. */
|
|
210
|
+
email: SenderConfig;
|
|
211
|
+
/** The render subsystem: the one renderer, its directive vocabulary, and its icons. */
|
|
212
|
+
rendering: {
|
|
213
|
+
/**
|
|
214
|
+
* The one renderer the editor preview and every public page call (design decision 4). `resolve`
|
|
215
|
+
* rewrites cairn: links to live permalinks; the build passes a site-resolver-backed one, the
|
|
216
|
+
* preview a manifest one. `resolveMedia` resolves media: references the same way.
|
|
217
|
+
*/
|
|
218
|
+
render: SiteRender;
|
|
219
|
+
/** Directive component registry; the renderer and the insert palette derive from it (seam 3). */
|
|
220
|
+
components?: ComponentRegistry;
|
|
221
|
+
/** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
|
|
222
|
+
icons?: IconSet;
|
|
223
|
+
/**
|
|
224
|
+
* The live Svelte components for hydrated directives, keyed by directive name (phase 4b islands).
|
|
225
|
+
* Every component whose {@link ComponentDef.hydrate} is set needs an entry here, and every entry
|
|
226
|
+
* needs a matching `hydrate` component; `defineAdapter` checks both. Absent leaves the site
|
|
227
|
+
* static, and the island client runtime is never imported.
|
|
228
|
+
*/
|
|
229
|
+
islands?: IslandRegistry;
|
|
230
|
+
};
|
|
231
|
+
/** R2-backed media (seam 4): the bucket binding and image variants. Absent leaves media off. */
|
|
232
|
+
media?: AssetConfig;
|
|
233
|
+
/** Admin-experience knobs: the preview frame, the nav menu, and the editor support contact. */
|
|
234
|
+
editor?: {
|
|
235
|
+
/**
|
|
236
|
+
* The live site's content styling for the preview frame. The admin's chrome isolation keeps
|
|
237
|
+
* the site's CSS out of the admin document, so the preview frame links these instead.
|
|
238
|
+
*/
|
|
239
|
+
preview?: PreviewConfig;
|
|
240
|
+
/** Which git-committed YAML menu the nav editor manages. */
|
|
241
|
+
nav?: NavMenuConfig;
|
|
242
|
+
/**
|
|
243
|
+
* Optional contact a stuck editor is pointed to from the in-admin help (an email address, a URL,
|
|
244
|
+
* or a name and instruction). The help renders the hand-off only when this is set. Plain string,
|
|
245
|
+
* passed through verbatim.
|
|
246
|
+
*/
|
|
247
|
+
supportContact?: string;
|
|
269
248
|
};
|
|
270
|
-
backend: BackendConfig;
|
|
271
|
-
sender: SenderConfig;
|
|
272
|
-
/**
|
|
273
|
-
* Optional contact a stuck editor is pointed to from the in-admin help (an email address, a URL,
|
|
274
|
-
* or a name and instruction). The help renders the hand-off only when this is set. Plain string,
|
|
275
|
-
* passed through verbatim.
|
|
276
|
-
*/
|
|
277
|
-
supportContact?: string;
|
|
278
|
-
/**
|
|
279
|
-
* The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
280
|
-
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-resolver-backed
|
|
281
|
-
* one, the preview a manifest one. The trailing `resolveMedia` is additive and optional: the build
|
|
282
|
-
* passes a site-resolver-backed media resolver, the preview a manifest-backed one.
|
|
283
|
-
*/
|
|
284
|
-
render(
|
|
285
|
-
md: string,
|
|
286
|
-
opts?: {
|
|
287
|
-
stagger?: boolean;
|
|
288
|
-
resolve?: LinkResolve;
|
|
289
|
-
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
290
|
-
},
|
|
291
|
-
): string | Promise<string>;
|
|
292
|
-
/**
|
|
293
|
-
* Repo-relative path to the committed content manifest. Defaults to src/content/.cairn/index.json
|
|
294
|
-
* in composeRuntime. It sits outside any concept directory, so content enumeration never globs it.
|
|
295
|
-
*/
|
|
296
|
-
manifestPath?: string;
|
|
297
|
-
/**
|
|
298
|
-
* Repo-relative path to the committed media manifest. Defaults to src/content/.cairn/media.json,
|
|
299
|
-
* applied in composeRuntime. Sits outside any concept directory, like the content manifest.
|
|
300
|
-
*/
|
|
301
|
-
mediaManifestPath?: string;
|
|
302
|
-
/**
|
|
303
|
-
* Repo-relative path to the committed personal dictionary file. Defaults to
|
|
304
|
-
* src/content/.cairn/dictionary.txt, applied in composeRuntime: the same `.cairn/` content root the
|
|
305
|
-
* manifests use, so the spec's `content/.cairn/dictionary.txt` resolves the same configurable way the
|
|
306
|
-
* manifest paths do. One word per line, sorted, comment lines allowed (see site-dictionary.ts).
|
|
307
|
-
*/
|
|
308
|
-
dictionaryPath?: string;
|
|
309
|
-
/** Directive component registry; the renderer and the future palette derive from it (seam 3). */
|
|
310
|
-
registry?: ComponentRegistry;
|
|
311
|
-
/** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
|
|
312
|
-
icons?: IconSet;
|
|
313
|
-
navMenu?: NavMenuConfig;
|
|
314
|
-
/**
|
|
315
|
-
* The live site's content styling for the preview frame. The admin's chrome isolation keeps
|
|
316
|
-
* the site's CSS out of the admin document, so the preview frame links these instead.
|
|
317
|
-
*/
|
|
318
|
-
preview?: PreviewConfig;
|
|
319
|
-
assets?: AssetConfig;
|
|
320
249
|
}
|
|
321
250
|
|
|
322
251
|
/**
|
|
@@ -351,7 +280,17 @@ export interface ConceptDescriptor {
|
|
|
351
280
|
permalink: string;
|
|
352
281
|
/** Filename date-prefix granularity for a dated concept; resolved by `normalizeConcepts`. */
|
|
353
282
|
datePrefix: DatePrefix;
|
|
354
|
-
|
|
283
|
+
/**
|
|
284
|
+
* The concept's fields in normalized form: each descriptor with its record key re-attached as
|
|
285
|
+
* `name`, derived by `normalizeConcepts` from the concept's `fieldset` record. Every consumer
|
|
286
|
+
* (the editor form, the form decoder, the media extractor) iterates this array and reads `name`.
|
|
287
|
+
*/
|
|
288
|
+
fields: NamedField[];
|
|
289
|
+
/**
|
|
290
|
+
* The concept's source fieldset, carried through so `editLoad` can resolve a create-form's
|
|
291
|
+
* initial values (a `default: 'today'` date) against a request-time clock via `initialValues`.
|
|
292
|
+
*/
|
|
293
|
+
schema: Fieldset;
|
|
355
294
|
/**
|
|
356
295
|
* Frontmatter keys the index copies onto each summary's `fields` record. `normalizeConcepts`
|
|
357
296
|
* resolves it to `[]` when a concept omits `summaryFields`.
|
|
@@ -412,23 +351,17 @@ export interface CairnExtension {
|
|
|
412
351
|
export interface CairnRuntime {
|
|
413
352
|
siteName: string;
|
|
414
353
|
concepts: ConceptDescriptor[];
|
|
415
|
-
backend
|
|
354
|
+
/** The commit backend provider, carried through from the adapter by `composeRuntime`. */
|
|
355
|
+
backend: BackendProvider;
|
|
416
356
|
sender: SenderConfig;
|
|
417
357
|
/** The support contact passed through from the adapter; the in-admin help reads it. Optional. */
|
|
418
358
|
supportContact?: string;
|
|
419
359
|
/**
|
|
420
360
|
* The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
421
|
-
* The
|
|
422
|
-
*
|
|
361
|
+
* The build passes a site-resolver-backed `resolve`/`resolveMedia` pair, the preview manifest-backed
|
|
362
|
+
* ones.
|
|
423
363
|
*/
|
|
424
|
-
render
|
|
425
|
-
md: string,
|
|
426
|
-
opts?: {
|
|
427
|
-
stagger?: boolean;
|
|
428
|
-
resolve?: LinkResolve;
|
|
429
|
-
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
430
|
-
},
|
|
431
|
-
): string | Promise<string>;
|
|
364
|
+
render: SiteRender;
|
|
432
365
|
manifestPath: string;
|
|
433
366
|
/** The repo-relative path to the committed media manifest, defaulted in composeRuntime. */
|
|
434
367
|
mediaManifestPath: string;
|
package/src/lib/delivery/data.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// the graph. The full ./delivery barrel re-exports this and adds the route loaders.
|
|
4
4
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
5
5
|
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
6
|
-
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
7
|
-
export type { SiteResolver, ConceptIndex } from './site-resolver.js';
|
|
6
|
+
export { createSiteResolver, buildLinkResolver, resolveReferences } from './site-resolver.js';
|
|
7
|
+
export type { SiteResolver, ConceptIndex, ResolvedReference } from './site-resolver.js';
|
|
8
8
|
export { createSiteIndexes } from './site-indexes.js';
|
|
9
9
|
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
10
10
|
export { siteDescriptors } from './site-descriptors.js';
|
|
@@ -10,7 +10,7 @@ import { buildSeoMeta } from './seo.js';
|
|
|
10
10
|
import type { SeoMeta } from './seo.js';
|
|
11
11
|
import { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
12
12
|
import { buildLinkResolver } from './site-resolver.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type { SiteRender } from '../content/types.js';
|
|
14
14
|
import type { MediaResolve } from '../render/resolve-media.js';
|
|
15
15
|
import { parseMediaToken } from '../media/reference.js';
|
|
16
16
|
import { log } from '../log/index.js';
|
|
@@ -18,7 +18,7 @@ import { log } from '../log/index.js';
|
|
|
18
18
|
/** Injected dependencies for the public loaders. */
|
|
19
19
|
export interface PublicRoutesDeps {
|
|
20
20
|
site: SiteResolver;
|
|
21
|
-
render:
|
|
21
|
+
render: SiteRender;
|
|
22
22
|
origin: string;
|
|
23
23
|
/** Site name for og:site_name and the SEO head. */
|
|
24
24
|
siteName: string;
|
|
@@ -163,7 +163,21 @@ export function createPublicRoutes(deps: PublicRoutesDeps) {
|
|
|
163
163
|
...(fields.author ? { author: fields.author } : {}),
|
|
164
164
|
...(entry.date ? { feeds } : {}),
|
|
165
165
|
});
|
|
166
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
concept: entry.concept,
|
|
168
|
+
entry,
|
|
169
|
+
html: await render({
|
|
170
|
+
body: entry.body,
|
|
171
|
+
concept: entry.concept,
|
|
172
|
+
frontmatter: entry.frontmatter,
|
|
173
|
+
resolve: buildLinkResolver(site),
|
|
174
|
+
}),
|
|
175
|
+
canonicalUrl,
|
|
176
|
+
seo,
|
|
177
|
+
newer,
|
|
178
|
+
older,
|
|
179
|
+
...(heroImage ? { heroImage } : {}),
|
|
180
|
+
};
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
/** The chronological archive for one concept: every non-draft summary, newest-first. */
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept descriptors
|
|
2
2
|
// the admin runtime uses; this delegates to the shared resolveConcepts so the pairing is one path, not
|
|
3
|
-
// tribal knowledge.
|
|
3
|
+
// tribal knowledge. Each concept declares its own routing and URL policy, the single source of truth.
|
|
4
4
|
import { resolveConcepts } from '../content/concepts.js';
|
|
5
5
|
import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
|
|
6
6
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Per-concept descriptors for a site, from its adapter content. The `siteConfig` parameter is retained
|
|
10
|
+
* for API stability and the menus and site name it still carries; the URL policy now lives on each
|
|
11
|
+
* concept, so it is not read here.
|
|
12
|
+
*/
|
|
9
13
|
export function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[] {
|
|
10
|
-
|
|
14
|
+
void siteConfig;
|
|
15
|
+
return resolveConcepts(adapter.content);
|
|
11
16
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// lower-level escape hatch. It imports only pure content and delivery code, so the delivery
|
|
6
6
|
// bundle stays backend-free.
|
|
7
7
|
import type { CairnAdapter, ConceptConfig } from '../content/types.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type { InferFieldset } from '../content/fieldset.js';
|
|
9
9
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
10
10
|
import { siteDescriptors } from './site-descriptors.js';
|
|
11
11
|
import { createContentIndex, fromGlob } from './content-index.js';
|
|
@@ -24,7 +24,7 @@ export type SiteGlobs<A extends CairnAdapter> = {
|
|
|
24
24
|
*/
|
|
25
25
|
export type SiteIndexes<A extends CairnAdapter> = {
|
|
26
26
|
[K in keyof A['content']]: ContentIndex<
|
|
27
|
-
NonNullable<A['content'][K]> extends ConceptConfig<infer S> ?
|
|
27
|
+
NonNullable<A['content'][K]> extends ConceptConfig<infer S> ? InferFieldset<S> : Record<string, unknown>
|
|
28
28
|
>;
|
|
29
29
|
} & { readonly site: SiteResolver };
|
|
30
30
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
7
7
|
import type { ContentEntry, ContentIndex, ContentSummary } from './content-index.js';
|
|
8
8
|
import type { LinkResolve } from '../content/links.js';
|
|
9
|
+
import { extractReferenceEdges, type ReferenceEdge } from '../content/references.js';
|
|
9
10
|
|
|
10
11
|
/** One concept's descriptor paired with its built index. */
|
|
11
12
|
export interface ConceptIndex {
|
|
@@ -93,6 +94,69 @@ export function createSiteResolver(concepts: ConceptIndex[], opts: { validate?:
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A reference edge resolved to its target's identity, for a public route to render a linked target.
|
|
99
|
+
* It reuses the target entry's own summary fields rather than re-deriving them, so a linked author
|
|
100
|
+
* card reads the same title and permalink the target's own page does. `summary` is the target's
|
|
101
|
+
* excerpt when present.
|
|
102
|
+
*/
|
|
103
|
+
export interface ResolvedReference {
|
|
104
|
+
id: string;
|
|
105
|
+
concept: string;
|
|
106
|
+
title: string;
|
|
107
|
+
permalink: string;
|
|
108
|
+
summary?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Project a resolved target entry into the identity a public route renders for a reference. */
|
|
112
|
+
function projectReference(edge: ReferenceEdge, target: ContentSummary): ResolvedReference {
|
|
113
|
+
const resolved: ResolvedReference = {
|
|
114
|
+
id: edge.id,
|
|
115
|
+
concept: edge.concept,
|
|
116
|
+
title: target.title,
|
|
117
|
+
permalink: target.permalink,
|
|
118
|
+
};
|
|
119
|
+
if (target.excerpt) resolved.summary = target.excerpt;
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a concept's `reference` and `array(reference)` frontmatter edges to their target identities,
|
|
125
|
+
* keyed by the field name, so a public route renders a reference as a link to its target's page. The
|
|
126
|
+
* resolution lives here because only the cross-concept resolver reaches a different concept's entries:
|
|
127
|
+
* a posts entry's `author` edge targets a pages entry, which the posts index alone cannot read. A
|
|
128
|
+
* single `reference` field resolves to one `ResolvedReference`, an `array(reference)` to a
|
|
129
|
+
* `ResolvedReference[]` in edge order. An id with no live target is dropped rather than thrown: the
|
|
130
|
+
* build's `verifyReferences` gate already fails a true dangling edge, so an unresolved id at request
|
|
131
|
+
* time is a mid-flight or draft target, not a hard error. Resolve per call, since the target entries
|
|
132
|
+
* exist only after every per-concept index is unioned into the resolver.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveReferences(
|
|
135
|
+
site: SiteResolver,
|
|
136
|
+
descriptor: ConceptDescriptor,
|
|
137
|
+
frontmatter: Record<string, unknown>,
|
|
138
|
+
): Record<string, ResolvedReference | ResolvedReference[]> {
|
|
139
|
+
const edges = extractReferenceEdges(frontmatter, descriptor.fields);
|
|
140
|
+
const resolved: Record<string, ResolvedReference | ResolvedReference[]> = {};
|
|
141
|
+
for (const field of descriptor.fields) {
|
|
142
|
+
const isSingle = field.type === 'reference';
|
|
143
|
+
const isArray = field.type === 'array' && field.item.type === 'reference';
|
|
144
|
+
if (!isSingle && !isArray) continue;
|
|
145
|
+
const fieldEdges = edges.filter((edge) => edge.field === field.name);
|
|
146
|
+
const hits: ResolvedReference[] = [];
|
|
147
|
+
for (const edge of fieldEdges) {
|
|
148
|
+
const target = site.concept(edge.concept)?.byId(edge.id);
|
|
149
|
+
if (target) hits.push(projectReference(edge, target));
|
|
150
|
+
}
|
|
151
|
+
if (isSingle) {
|
|
152
|
+
if (hits.length > 0) resolved[field.name] = hits[0];
|
|
153
|
+
} else {
|
|
154
|
+
resolved[field.name] = hits;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return resolved;
|
|
158
|
+
}
|
|
159
|
+
|
|
96
160
|
/**
|
|
97
161
|
* A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
98
162
|
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks.
|
|
@@ -5,11 +5,8 @@ import { fail, pass, skip } from './types.js';
|
|
|
5
5
|
import type { CheckResult, DoctorCheck, DoctorContext } from './types.js';
|
|
6
6
|
import { readWranglerConfig } from './wrangler-config.js';
|
|
7
7
|
import { requireOrigin } from '../env.js';
|
|
8
|
-
import { parseSiteConfig
|
|
8
|
+
import { parseSiteConfig } from '../nav/site-config.js';
|
|
9
9
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
10
|
-
import { normalizeConcepts } from '../content/concepts.js';
|
|
11
|
-
import { defineFields } from '../content/schema.js';
|
|
12
|
-
import type { ConceptConfig } from '../content/types.js';
|
|
13
10
|
|
|
14
11
|
const NO_WRANGLER: CheckResult = skip('no wrangler.jsonc or wrangler.toml found');
|
|
15
12
|
|
|
@@ -156,16 +153,11 @@ export const configSiteConfig: DoctorCheck = {
|
|
|
156
153
|
const text = await readSiteConfigText(ctx);
|
|
157
154
|
if (text === null) return skip(`no site.config.yaml found (looked in ${SITE_CONFIG_PATHS.join(', ')})`);
|
|
158
155
|
try {
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const synthetic = Object.fromEntries(
|
|
165
|
-
Object.keys(policy).map((id): [string, ConceptConfig] => [id, { dir: '', schema: defineFields([]) }])
|
|
166
|
-
);
|
|
167
|
-
normalizeConcepts(synthetic, policy);
|
|
168
|
-
return pass('parsed and URL policy validated (the adapter concept set is not checkable from the CLI)');
|
|
156
|
+
// Parse-only. parseSiteConfig validates the root shape and, since Contract v2, hard-errors on a
|
|
157
|
+
// stale per-concept `content:` block (URL policy moved onto defineConcept). The per-concept URL
|
|
158
|
+
// policy is now validated at the concept declaration, which a CLI cannot reach without the adapter.
|
|
159
|
+
parseSiteConfig(text);
|
|
160
|
+
return pass('parsed (per-concept URL policy lives on the adapter concepts, not checkable from the CLI)');
|
|
169
161
|
} catch (err) {
|
|
170
162
|
return fail(err instanceof Error ? err.message : String(err));
|
|
171
163
|
}
|