@glw907/cairn-cms 0.62.2 → 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 +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- 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 +179 -2
- 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.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- 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 +10 -5
- package/dist/delivery/public-routes.js +25 -2
- 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 +18 -10
- package/dist/index.js +9 -5
- 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/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -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/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -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/guard.js +10 -0
- 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 +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.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 +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- 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 +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- 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 +36 -4
- 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 +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -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/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- 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 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- package/src/lib/content/validate.ts +0 -90
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { PENDING_PREFIX, parsePendingBranch } from './pending.js';
|
|
2
|
+
import { findConcept } from './concepts.js';
|
|
3
|
+
import { isValidId, filenameFromId } from './ids.js';
|
|
4
|
+
import { parseMarkdown } from './frontmatter.js';
|
|
5
|
+
import { extractReferenceEdges } from './references.js';
|
|
6
|
+
/** Append a row under its target pair key, creating the bucket on first use. */
|
|
7
|
+
function push(index, key, entry) {
|
|
8
|
+
const rows = index.get(key);
|
|
9
|
+
if (rows)
|
|
10
|
+
rows.push(entry);
|
|
11
|
+
else
|
|
12
|
+
index.set(key, [entry]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the pair-keyed reference index over main (from the manifest's per-entry references) plus every
|
|
16
|
+
* open cairn/* branch (parsed from its edited markdown).
|
|
17
|
+
*
|
|
18
|
+
* By default a single branch read that throws degrades that one branch and is skipped, the way the
|
|
19
|
+
* admin loaders degrade a failed read. That tolerance is wrong for the rename and delete gates: a
|
|
20
|
+
* transient branch-read failure would make a still-referenced target look free. Pass `strict: true` to
|
|
21
|
+
* rethrow a branch failure so the caller fails closed. Pass `branches` to reuse a branch list the
|
|
22
|
+
* caller already has rather than listing them a second time.
|
|
23
|
+
*/
|
|
24
|
+
export async function buildReferenceIndex(backend, concepts, manifest, opts = {}) {
|
|
25
|
+
const index = new Map();
|
|
26
|
+
// The main arm: the manifest already carries each entry's reference edges, so this is a pure reverse
|
|
27
|
+
// map with no per-file read. The KEY is the edge's TARGET (concept, id); the ROW is the source entry.
|
|
28
|
+
for (const entry of manifest.entries) {
|
|
29
|
+
for (const edge of entry.references ?? []) {
|
|
30
|
+
push(index, `${edge.concept}/${edge.id}`, {
|
|
31
|
+
concept: entry.concept,
|
|
32
|
+
id: entry.id,
|
|
33
|
+
title: entry.title,
|
|
34
|
+
permalink: entry.permalink,
|
|
35
|
+
field: edge.field,
|
|
36
|
+
origin: { kind: 'published' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// The branch arm: read each open cairn/* branch's one edited file. The path is derivable from the
|
|
41
|
+
// branch name, so no tree-listing is needed. The branch list is reused when the caller passes it.
|
|
42
|
+
const names = opts.branches ?? (await backend.listBranches(PENDING_PREFIX));
|
|
43
|
+
// Read the branches in parallel rather than one at a time, so the latency floor is one round trip
|
|
44
|
+
// instead of N. workerd self-throttles to 6 simultaneous outbound connections, so this batch and
|
|
45
|
+
// the load path's media-union and linker reads each stay under the limit; do NOT run this fan-out
|
|
46
|
+
// concurrently with those (a future combined safety gate must not wrap them in one Promise.all),
|
|
47
|
+
// since the merged fan-out would queue behind that throttle.
|
|
48
|
+
const perBranch = await Promise.all(names.map(async (name) => {
|
|
49
|
+
// Resolve the branch name to a configured entry with the same guard the branch tooling uses: a
|
|
50
|
+
// malformed name, an id that fails the slug rule (entry paths are built from it, so this is the
|
|
51
|
+
// path confinement), or a concept this site does not configure is skipped, no read attempted.
|
|
52
|
+
const ref = parsePendingBranch(name);
|
|
53
|
+
if (!ref || !isValidId(ref.id))
|
|
54
|
+
return [];
|
|
55
|
+
const concept = findConcept(concepts, ref.concept);
|
|
56
|
+
if (!concept)
|
|
57
|
+
return [];
|
|
58
|
+
const path = `${concept.dir}/${filenameFromId(ref.id)}`;
|
|
59
|
+
try {
|
|
60
|
+
const raw = await backend.readFile(path, name);
|
|
61
|
+
if (raw === null)
|
|
62
|
+
return []; // The file is absent on the branch: nothing to extract.
|
|
63
|
+
const { frontmatter } = parseMarkdown(raw);
|
|
64
|
+
const fmTitle = frontmatter.title;
|
|
65
|
+
const title = typeof fmTitle === 'string' && fmTitle.trim() ? fmTitle : ref.id;
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const edge of extractReferenceEdges(frontmatter, concept.fields)) {
|
|
68
|
+
rows.push({
|
|
69
|
+
key: `${edge.concept}/${edge.id}`,
|
|
70
|
+
entry: {
|
|
71
|
+
concept: concept.id,
|
|
72
|
+
id: ref.id,
|
|
73
|
+
title,
|
|
74
|
+
field: edge.field,
|
|
75
|
+
origin: { kind: 'branch', branch: name },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// In strict mode a branch failure fails the whole build so the gate can fail closed; otherwise
|
|
83
|
+
// degrade this one branch rather than sinking the screen.
|
|
84
|
+
if (opts.strict)
|
|
85
|
+
throw err;
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
// Fold the per-branch rows back in, preserving the branch order so the index reads stably.
|
|
90
|
+
for (const rows of perBranch) {
|
|
91
|
+
for (const { key, entry } of rows)
|
|
92
|
+
push(index, key, entry);
|
|
93
|
+
}
|
|
94
|
+
return index;
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { NamedField } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One typed frontmatter edge from a content entry to a target entry: the `field` the edge was
|
|
4
|
+
* declared on, the target's `concept`, and the target's permanent `id`. The manifest records these
|
|
5
|
+
* per entry, the cross-branch index reverse-maps them, and the build verifies each one resolves.
|
|
6
|
+
*/
|
|
7
|
+
export interface ReferenceEdge {
|
|
8
|
+
/** The frontmatter key the edge was declared on. */
|
|
9
|
+
field: string;
|
|
10
|
+
/** The target's concept, taken from the field descriptor, never the value. */
|
|
11
|
+
concept: string;
|
|
12
|
+
/** The target's permanent id. */
|
|
13
|
+
id: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read every reference edge a parsed entry declares against its concept's fields. A `reference` field
|
|
17
|
+
* yields one edge from the shared canonicalizer; an `array` of `reference` yields one edge per
|
|
18
|
+
* canonicalized element, with the target `concept` taken from the descriptor rather than the value so
|
|
19
|
+
* a hand-edited file cannot misdirect an edge. Each id must pass `isValidId`, and edges are emitted in
|
|
20
|
+
* field then array order with duplicates dropped. The value is coerced through `referenceIdsFromValue`,
|
|
21
|
+
* never iterated directly: a lone scalar of an array field is one id, never a string walked
|
|
22
|
+
* character by character into per-character garbage edges.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractReferenceEdges(frontmatter: Record<string, unknown>, fields: NamedField[]): ReferenceEdge[];
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite a token-bounded `oldId` to `newId` within one top-level frontmatter key's value, returning
|
|
27
|
+
* the source byte-for-byte identical apart from that substring. The key's line range is found with the
|
|
28
|
+
* colon-anchored `frontmatterKeyRange` (so `author` never matches `authored-by:`), and within each
|
|
29
|
+
* line only the value side, an inline `# comment` left intact, is scanned. The token boundary is
|
|
30
|
+
* `(?<![a-z0-9-])`/`(?![a-z0-9-])` rather than `\b`, since ids contain hyphens, so a substring id is
|
|
31
|
+
* not matched. A `newId` that would not reparse as a YAML string (`true`, `123`, a date-shaped
|
|
32
|
+
* `2026-01-02`) is written single-quoted, otherwise a raw substitution reparses as a boolean, number,
|
|
33
|
+
* or Date and `coerceToText`/`extractReferenceEdges` silently drop the edge. When the matched `oldId`
|
|
34
|
+
* is itself wrapped in an existing quote pair (the form serializeMarkdown commits a significant id in,
|
|
35
|
+
* `author: '123'`), the splice extends over both quote bytes so the self-quoting replacement does not
|
|
36
|
+
* nest a second pair into invalid `''true''`. A leading BOM, every
|
|
37
|
+
* `\r`, and a source with no frontmatter, an absent field, or a malformed `oldId`/`newId` are
|
|
38
|
+
* preserved unchanged. Pure and node-safe.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rewriteFrontmatterReference(source: string, field: string, oldId: string, newId: string): string;
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The validate input the cairn adapter takes: the raw frontmatter and the body. */
|
|
2
|
+
export interface StandardInput {
|
|
3
|
+
frontmatter: Record<string, unknown>;
|
|
4
|
+
body: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
|
|
8
|
+
* schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
|
|
9
|
+
*/
|
|
10
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
11
|
+
readonly '~standard': {
|
|
12
|
+
readonly version: 1;
|
|
13
|
+
readonly vendor: string;
|
|
14
|
+
readonly validate: (value: unknown) => StandardResult<Output>;
|
|
15
|
+
readonly types?: {
|
|
16
|
+
readonly input: Input;
|
|
17
|
+
readonly output: Output;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
type StandardResult<Output> = {
|
|
22
|
+
readonly value: Output;
|
|
23
|
+
readonly issues?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
readonly issues: ReadonlyArray<{
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly path?: ReadonlyArray<PropertyKey>;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// cairn-cms: the Standard Schema conformance types, shared by both the v1 schema and the v2
|
|
2
|
+
// fieldset validators. They live here, apart from either validator, so the v2 `fieldset` keeps
|
|
3
|
+
// importing them once the v1 `schema.ts` is removed at the Contract v2 cutover.
|
|
4
|
+
export {};
|
package/dist/content/types.d.ts
CHANGED
|
@@ -1,95 +1,12 @@
|
|
|
1
1
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
2
|
import type { IconSet } from '../render/glyph.js';
|
|
3
|
+
import type { IslandRegistry } from '../islands/types.js';
|
|
4
|
+
import type { BackendProvider } from '../github/backend.js';
|
|
3
5
|
import type { DatePrefix } from './ids.js';
|
|
4
|
-
import type {
|
|
6
|
+
import type { Fieldset } from './fieldset.js';
|
|
7
|
+
import type { FieldDescriptor } from './fields.js';
|
|
5
8
|
import type { LinkResolve } from './links.js';
|
|
6
9
|
import type { VariantSpec } from '../media/transform-url.js';
|
|
7
|
-
/** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
|
|
8
|
-
interface FieldBase {
|
|
9
|
-
/** Frontmatter key and form input name. */
|
|
10
|
-
name: string;
|
|
11
|
-
/** Form label. */
|
|
12
|
-
label: string;
|
|
13
|
-
/** A required field fails validation when empty (spec §7.4). */
|
|
14
|
-
required?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
* One author-facing sentence shown under the field in the editor, in plain end-user language.
|
|
17
|
-
* Optional; render nothing when absent. Not a validation rule.
|
|
18
|
-
*/
|
|
19
|
-
description?: string;
|
|
20
|
-
}
|
|
21
|
-
/** A single-line text input. */
|
|
22
|
-
export interface TextField extends FieldBase {
|
|
23
|
-
type: 'text';
|
|
24
|
-
/** Minimum character length of a non-empty value. */
|
|
25
|
-
min?: number;
|
|
26
|
-
/** Maximum character length. */
|
|
27
|
-
max?: number;
|
|
28
|
-
/** Exact required character length. */
|
|
29
|
-
length?: number;
|
|
30
|
-
/**
|
|
31
|
-
* A regular-expression source string the value must match. Stored as a string so the field
|
|
32
|
-
* list stays plain serializable data; the validator compiles it.
|
|
33
|
-
*/
|
|
34
|
-
pattern?: string;
|
|
35
|
-
}
|
|
36
|
-
/** A multi-line text input. */
|
|
37
|
-
export interface TextareaField extends FieldBase {
|
|
38
|
-
type: 'textarea';
|
|
39
|
-
/** Visible rows; the editor picks a default when omitted. */
|
|
40
|
-
rows?: number;
|
|
41
|
-
/** Minimum character length of a non-empty value. */
|
|
42
|
-
min?: number;
|
|
43
|
-
/** Maximum character length. */
|
|
44
|
-
max?: number;
|
|
45
|
-
/** Exact required character length. */
|
|
46
|
-
length?: number;
|
|
47
|
-
/** A regular-expression source string the value must match. */
|
|
48
|
-
pattern?: string;
|
|
49
|
-
}
|
|
50
|
-
/** A `YYYY-MM-DD` date input. */
|
|
51
|
-
export interface DateField extends FieldBase {
|
|
52
|
-
type: 'date';
|
|
53
|
-
/** Earliest allowed date, as `YYYY-MM-DD`. */
|
|
54
|
-
min?: string;
|
|
55
|
-
/** Latest allowed date, as `YYYY-MM-DD`. */
|
|
56
|
-
max?: string;
|
|
57
|
-
}
|
|
58
|
-
/** A checkbox; absent means false. */
|
|
59
|
-
export interface BooleanField extends FieldBase {
|
|
60
|
-
type: 'boolean';
|
|
61
|
-
}
|
|
62
|
-
/** A closed-vocabulary tag set, rendered as checkboxes (ecnordic). */
|
|
63
|
-
export interface TagsField extends FieldBase {
|
|
64
|
-
type: 'tags';
|
|
65
|
-
/** The controlled vocabulary. */
|
|
66
|
-
options: readonly string[];
|
|
67
|
-
}
|
|
68
|
-
/** Free-form tags, edited as one comma-separated input (907). */
|
|
69
|
-
export interface FreeTagsField extends FieldBase {
|
|
70
|
-
type: 'freetags';
|
|
71
|
-
placeholder?: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* A hero image set in frontmatter. The stored value is the nested object
|
|
75
|
-
* `{ src: string; alt: string; caption?: string }`, where `src` is a 2b `media:` reference, `alt`
|
|
76
|
-
* is the screen-reader description, and `caption` is an optional line the site template may show.
|
|
77
|
-
* One image serves two jobs: the template's lead image and the social-card image. The field feeding
|
|
78
|
-
* the social card is the `seo`-flagged one, defaulting to the field named `image`; a concept declares
|
|
79
|
-
* at most one SEO image field.
|
|
80
|
-
*/
|
|
81
|
-
export interface ImageField extends FieldBase {
|
|
82
|
-
type: 'image';
|
|
83
|
-
/** Whether this field feeds the social-card image. The field named `image` defaults to true. */
|
|
84
|
-
seo?: boolean;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* The discriminated union the per-concept frontmatter form is generated from. A scalar field type
|
|
88
|
-
* is one variant here plus one decode arm in `frontmatterFromForm` and one in `validateFields`. The
|
|
89
|
-
* structured `image` field additionally needs a read-back arm in `formValues` and a type-inference
|
|
90
|
-
* arm in `schema.ts`, since its value is a nested object rather than a single string.
|
|
91
|
-
*/
|
|
92
|
-
export type FrontmatterField = TextField | TextareaField | DateField | BooleanField | TagsField | FreeTagsField | ImageField;
|
|
93
10
|
/**
|
|
94
11
|
* The stored value of an `image` field: a `media:` reference, a screen-reader description, and an
|
|
95
12
|
* optional caption.
|
|
@@ -101,10 +18,18 @@ export interface ImageValue {
|
|
|
101
18
|
/** An explicit decorative choice: an empty alt that is not debt. Omitted unless true. */
|
|
102
19
|
decorative?: boolean;
|
|
103
20
|
}
|
|
21
|
+
/** One validation failure located by a path: a top-level key, then a row index and/or a leaf sub-key. */
|
|
22
|
+
export interface ValidationIssue {
|
|
23
|
+
/** The path to the failing field, e.g. ['faq', 0, 'question'] or ['address', 'city'] or ['title']. */
|
|
24
|
+
path: (string | number)[];
|
|
25
|
+
/** The author-facing message, naming the field's label. */
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
104
28
|
/**
|
|
105
|
-
* A validator's verdict. On success it carries the normalized frontmatter to commit; on
|
|
106
|
-
*
|
|
107
|
-
*
|
|
29
|
+
* A validator's verdict. On success it carries the normalized frontmatter to commit; on failure it
|
|
30
|
+
* carries field-keyed error messages (the empty key is a form-level error) and, additively, the
|
|
31
|
+
* located `issues` with multi-segment paths so the form can route a nested-container error to the
|
|
32
|
+
* right input. Invalid input bounces to the form and never reaches git (spec §7.4).
|
|
108
33
|
*/
|
|
109
34
|
export type ValidationResult = {
|
|
110
35
|
ok: true;
|
|
@@ -112,48 +37,58 @@ export type ValidationResult = {
|
|
|
112
37
|
} | {
|
|
113
38
|
ok: false;
|
|
114
39
|
errors: Record<string, string>;
|
|
40
|
+
issues?: ValidationIssue[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A field descriptor with its frontmatter key re-attached as `name`. This is the normalized form
|
|
44
|
+
* `ConceptDescriptor.fields` carries: `normalizeConcepts` derives it from a concept's `fieldset`
|
|
45
|
+
* record so every consumer (the editor form, the form decoder, the media extractor) iterates an
|
|
46
|
+
* array and reads `name` rather than the keyed record.
|
|
47
|
+
*/
|
|
48
|
+
export type NamedField = FieldDescriptor & {
|
|
49
|
+
name: string;
|
|
115
50
|
};
|
|
116
51
|
/**
|
|
117
|
-
* Per-site configuration for one content concept (spec §8). One `
|
|
118
|
-
* `
|
|
119
|
-
* inferred frontmatter type. Generic over the
|
|
120
|
-
* typed reads.
|
|
121
|
-
*
|
|
52
|
+
* Per-site configuration for one content concept (spec §8). One `fields` fieldset, built with
|
|
53
|
+
* `fieldset`, is the single source of truth for the editor form, the validator, and the
|
|
54
|
+
* inferred frontmatter type. Generic over the fieldset so a concept's concrete type survives for
|
|
55
|
+
* typed reads. A concept also declares its own routing and URL policy here (`routing`, `permalink`,
|
|
56
|
+
* `datePrefix`), resolved by `normalizeConcepts`.
|
|
122
57
|
*/
|
|
123
|
-
export interface ConceptConfig<S extends
|
|
58
|
+
export interface ConceptConfig<S extends Fieldset = Fieldset> {
|
|
124
59
|
/** Repo-relative content directory, e.g. "src/content/posts". */
|
|
125
60
|
dir: string;
|
|
126
61
|
/** Sidebar label; defaults from the concept id when omitted. */
|
|
127
62
|
label?: string;
|
|
128
63
|
/** The singular noun for the create affordances ("New post"); defaults to `label` when omitted. */
|
|
129
64
|
singular?: string;
|
|
130
|
-
/** The concept's
|
|
131
|
-
|
|
65
|
+
/** The concept's fieldset: the form projection, the generated validator, and the inferred type. */
|
|
66
|
+
fields: S;
|
|
67
|
+
/**
|
|
68
|
+
* This concept's routing. A named shorthand (`'feed'` dated and in feeds, `'page'` a routable
|
|
69
|
+
* static page, `'embedded'` not routable) or an explicit rule. Omitted means `'page'`.
|
|
70
|
+
*/
|
|
71
|
+
routing?: 'feed' | 'page' | 'embedded' | RoutingRule;
|
|
72
|
+
/** The permalink pattern, root-relative, e.g. `/blog/:year/:slug`. Defaults by concept id. */
|
|
73
|
+
permalink?: string;
|
|
74
|
+
/** Date-prefix granularity for a dated concept's id-to-slug stripping. Defaults to `day`. */
|
|
75
|
+
datePrefix?: DatePrefix;
|
|
132
76
|
/**
|
|
133
77
|
* Frontmatter keys to surface on each `ContentSummary.fields`, so a list card reads an authored
|
|
134
|
-
* field without a per-entry detail read. Each key should also be declared in `
|
|
78
|
+
* field without a per-entry detail read. Each key should also be declared in `fields`.
|
|
135
79
|
*/
|
|
136
80
|
summaryFields?: string[];
|
|
137
81
|
}
|
|
138
82
|
/**
|
|
139
|
-
* A concept's URL policy,
|
|
140
|
-
* a `/`-prefixed pattern of literal
|
|
141
|
-
*
|
|
142
|
-
* `normalizeConcepts` when omitted.
|
|
83
|
+
* A concept's URL policy, declared on the adapter concept itself (`ConceptConfig.permalink` and
|
|
84
|
+
* `ConceptConfig.datePrefix`) since Contract v2. `permalink` is a `/`-prefixed pattern of literal
|
|
85
|
+
* segments and the tokens `:slug`, `:year`, `:month`, `:day`. `datePrefix` is the filename
|
|
86
|
+
* date-prefix granularity for a dated concept. Both default in `normalizeConcepts` when omitted.
|
|
143
87
|
*/
|
|
144
88
|
export interface ConceptUrlPolicy {
|
|
145
89
|
permalink?: string;
|
|
146
90
|
datePrefix?: DatePrefix;
|
|
147
91
|
}
|
|
148
|
-
/** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
|
|
149
|
-
export interface BackendConfig {
|
|
150
|
-
owner: string;
|
|
151
|
-
repo: string;
|
|
152
|
-
/** Commit target, e.g. "main". */
|
|
153
|
-
branch: string;
|
|
154
|
-
appId: string;
|
|
155
|
-
installationId: string;
|
|
156
|
-
}
|
|
157
92
|
/** Magic-link sender identity for Cloudflare Email Sending. */
|
|
158
93
|
export interface SenderConfig {
|
|
159
94
|
from: string;
|
|
@@ -233,64 +168,72 @@ export interface AssetConfig {
|
|
|
233
168
|
*/
|
|
234
169
|
transformations?: boolean;
|
|
235
170
|
}
|
|
236
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* The site's one renderer (design decision 4): the editor preview and every public page call it.
|
|
173
|
+
* Entry-aware so a custom renderer can vary output by concept or frontmatter; the default reads only
|
|
174
|
+
* `body` plus the resolvers. `resolve` rewrites cairn: links to live permalinks (the build passes a
|
|
175
|
+
* site-resolver-backed resolver, the preview a manifest-backed one); `resolveMedia` resolves media:
|
|
176
|
+
* references the same way. `concept` and `frontmatter` carry the entry's context for an entry render
|
|
177
|
+
* and are absent for the standalone component-insert preview.
|
|
178
|
+
*/
|
|
179
|
+
export type SiteRender = (input: {
|
|
180
|
+
body: string;
|
|
181
|
+
concept?: string;
|
|
182
|
+
frontmatter?: Record<string, unknown>;
|
|
183
|
+
resolve?: LinkResolve;
|
|
184
|
+
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
185
|
+
}) => Promise<string>;
|
|
186
|
+
/**
|
|
187
|
+
* The single seam the engine consumes. A site implements this at `src/lib/cairn.config.ts`, in six
|
|
188
|
+
* subsystem groups (spec §8): the content concepts, the commit backend, the magic-link sender, the
|
|
189
|
+
* render subsystem, the optional media stack, and the admin-experience knobs. The internal manifest
|
|
190
|
+
* and dictionary paths are not here; `composeRuntime` defaults them by convention.
|
|
191
|
+
*/
|
|
237
192
|
export interface CairnAdapter {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
193
|
+
/** The site's concepts, keyed by id. Posts and pages are the documented defaults; a site may add more. */
|
|
194
|
+
content: Record<string, ConceptConfig>;
|
|
195
|
+
/** The commit backend provider, from `githubApp({ ... })` (the GitHub App today). */
|
|
196
|
+
backend: BackendProvider;
|
|
197
|
+
/** The magic-link sender. */
|
|
198
|
+
email: SenderConfig;
|
|
199
|
+
/** The render subsystem: the one renderer, its directive vocabulary, and its icons. */
|
|
200
|
+
rendering: {
|
|
201
|
+
/**
|
|
202
|
+
* The one renderer the editor preview and every public page call (design decision 4). `resolve`
|
|
203
|
+
* rewrites cairn: links to live permalinks; the build passes a site-resolver-backed one, the
|
|
204
|
+
* preview a manifest one. `resolveMedia` resolves media: references the same way.
|
|
205
|
+
*/
|
|
206
|
+
render: SiteRender;
|
|
207
|
+
/** Directive component registry; the renderer and the insert palette derive from it (seam 3). */
|
|
208
|
+
components?: ComponentRegistry;
|
|
209
|
+
/** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
|
|
210
|
+
icons?: IconSet;
|
|
211
|
+
/**
|
|
212
|
+
* The live Svelte components for hydrated directives, keyed by directive name (phase 4b islands).
|
|
213
|
+
* Every component whose {@link ComponentDef.hydrate} is set needs an entry here, and every entry
|
|
214
|
+
* needs a matching `hydrate` component; `defineAdapter` checks both. Absent leaves the site
|
|
215
|
+
* static, and the island client runtime is never imported.
|
|
216
|
+
*/
|
|
217
|
+
islands?: IslandRegistry;
|
|
218
|
+
};
|
|
219
|
+
/** R2-backed media (seam 4): the bucket binding and image variants. Absent leaves media off. */
|
|
220
|
+
media?: AssetConfig;
|
|
221
|
+
/** Admin-experience knobs: the preview frame, the nav menu, and the editor support contact. */
|
|
222
|
+
editor?: {
|
|
223
|
+
/**
|
|
224
|
+
* The live site's content styling for the preview frame. The admin's chrome isolation keeps
|
|
225
|
+
* the site's CSS out of the admin document, so the preview frame links these instead.
|
|
226
|
+
*/
|
|
227
|
+
preview?: PreviewConfig;
|
|
228
|
+
/** Which git-committed YAML menu the nav editor manages. */
|
|
229
|
+
nav?: NavMenuConfig;
|
|
230
|
+
/**
|
|
231
|
+
* Optional contact a stuck editor is pointed to from the in-admin help (an email address, a URL,
|
|
232
|
+
* or a name and instruction). The help renders the hand-off only when this is set. Plain string,
|
|
233
|
+
* passed through verbatim.
|
|
234
|
+
*/
|
|
235
|
+
supportContact?: string;
|
|
246
236
|
};
|
|
247
|
-
backend: BackendConfig;
|
|
248
|
-
sender: SenderConfig;
|
|
249
|
-
/**
|
|
250
|
-
* Optional contact a stuck editor is pointed to from the in-admin help (an email address, a URL,
|
|
251
|
-
* or a name and instruction). The help renders the hand-off only when this is set. Plain string,
|
|
252
|
-
* passed through verbatim.
|
|
253
|
-
*/
|
|
254
|
-
supportContact?: string;
|
|
255
|
-
/**
|
|
256
|
-
* The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
257
|
-
* `resolve` rewrites cairn: links to live permalinks; the build passes a site-resolver-backed
|
|
258
|
-
* one, the preview a manifest one. The trailing `resolveMedia` is additive and optional: the build
|
|
259
|
-
* passes a site-resolver-backed media resolver, the preview a manifest-backed one.
|
|
260
|
-
*/
|
|
261
|
-
render(md: string, opts?: {
|
|
262
|
-
stagger?: boolean;
|
|
263
|
-
resolve?: LinkResolve;
|
|
264
|
-
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
265
|
-
}): string | Promise<string>;
|
|
266
|
-
/**
|
|
267
|
-
* Repo-relative path to the committed content manifest. Defaults to src/content/.cairn/index.json
|
|
268
|
-
* in composeRuntime. It sits outside any concept directory, so content enumeration never globs it.
|
|
269
|
-
*/
|
|
270
|
-
manifestPath?: string;
|
|
271
|
-
/**
|
|
272
|
-
* Repo-relative path to the committed media manifest. Defaults to src/content/.cairn/media.json,
|
|
273
|
-
* applied in composeRuntime. Sits outside any concept directory, like the content manifest.
|
|
274
|
-
*/
|
|
275
|
-
mediaManifestPath?: string;
|
|
276
|
-
/**
|
|
277
|
-
* Repo-relative path to the committed personal dictionary file. Defaults to
|
|
278
|
-
* src/content/.cairn/dictionary.txt, applied in composeRuntime: the same `.cairn/` content root the
|
|
279
|
-
* manifests use, so the spec's `content/.cairn/dictionary.txt` resolves the same configurable way the
|
|
280
|
-
* manifest paths do. One word per line, sorted, comment lines allowed (see site-dictionary.ts).
|
|
281
|
-
*/
|
|
282
|
-
dictionaryPath?: string;
|
|
283
|
-
/** Directive component registry; the renderer and the future palette derive from it (seam 3). */
|
|
284
|
-
registry?: ComponentRegistry;
|
|
285
|
-
/** The site's glyph name to SVG path-data map, for the admin icon picker and the renderer. */
|
|
286
|
-
icons?: IconSet;
|
|
287
|
-
navMenu?: NavMenuConfig;
|
|
288
|
-
/**
|
|
289
|
-
* The live site's content styling for the preview frame. The admin's chrome isolation keeps
|
|
290
|
-
* the site's CSS out of the admin document, so the preview frame links these instead.
|
|
291
|
-
*/
|
|
292
|
-
preview?: PreviewConfig;
|
|
293
|
-
assets?: AssetConfig;
|
|
294
237
|
}
|
|
295
238
|
/**
|
|
296
239
|
* Concept-fixed routing for a normalized concept (spec §7.2). Posts are dated feed entries;
|
|
@@ -323,7 +266,17 @@ export interface ConceptDescriptor {
|
|
|
323
266
|
permalink: string;
|
|
324
267
|
/** Filename date-prefix granularity for a dated concept; resolved by `normalizeConcepts`. */
|
|
325
268
|
datePrefix: DatePrefix;
|
|
326
|
-
|
|
269
|
+
/**
|
|
270
|
+
* The concept's fields in normalized form: each descriptor with its record key re-attached as
|
|
271
|
+
* `name`, derived by `normalizeConcepts` from the concept's `fieldset` record. Every consumer
|
|
272
|
+
* (the editor form, the form decoder, the media extractor) iterates this array and reads `name`.
|
|
273
|
+
*/
|
|
274
|
+
fields: NamedField[];
|
|
275
|
+
/**
|
|
276
|
+
* The concept's source fieldset, carried through so `editLoad` can resolve a create-form's
|
|
277
|
+
* initial values (a `default: 'today'` date) against a request-time clock via `initialValues`.
|
|
278
|
+
*/
|
|
279
|
+
schema: Fieldset;
|
|
327
280
|
/**
|
|
328
281
|
* Frontmatter keys the index copies onto each summary's `fields` record. `normalizeConcepts`
|
|
329
282
|
* resolves it to `[]` when a concept omits `summaryFields`.
|
|
@@ -380,20 +333,17 @@ export interface CairnExtension {
|
|
|
380
333
|
export interface CairnRuntime {
|
|
381
334
|
siteName: string;
|
|
382
335
|
concepts: ConceptDescriptor[];
|
|
383
|
-
backend
|
|
336
|
+
/** The commit backend provider, carried through from the adapter by `composeRuntime`. */
|
|
337
|
+
backend: BackendProvider;
|
|
384
338
|
sender: SenderConfig;
|
|
385
339
|
/** The support contact passed through from the adapter; the in-admin help reads it. Optional. */
|
|
386
340
|
supportContact?: string;
|
|
387
341
|
/**
|
|
388
342
|
* The site's one renderer: the editor preview and every public page call it (design decision 4).
|
|
389
|
-
* The
|
|
390
|
-
*
|
|
343
|
+
* The build passes a site-resolver-backed `resolve`/`resolveMedia` pair, the preview manifest-backed
|
|
344
|
+
* ones.
|
|
391
345
|
*/
|
|
392
|
-
render
|
|
393
|
-
stagger?: boolean;
|
|
394
|
-
resolve?: LinkResolve;
|
|
395
|
-
resolveMedia?: import('../render/resolve-media.js').MediaResolve;
|
|
396
|
-
}): string | Promise<string>;
|
|
346
|
+
render: SiteRender;
|
|
397
347
|
manifestPath: string;
|
|
398
348
|
/** The repo-relative path to the committed media manifest, defaulted in composeRuntime. */
|
|
399
349
|
mediaManifestPath: string;
|
|
@@ -440,4 +390,3 @@ export interface CairnRuntime {
|
|
|
440
390
|
/** Field types contributed by extensions (Mode 2). Empty until Plan 09 wires the form dispatch. */
|
|
441
391
|
fieldTypes?: FieldTypeDef[];
|
|
442
392
|
}
|
|
443
|
-
export {};
|
package/dist/delivery/data.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createContentIndex, fromGlob } from './content-index.js';
|
|
2
2
|
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
3
|
-
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
4
|
-
export type { SiteResolver, ConceptIndex } from './site-resolver.js';
|
|
3
|
+
export { createSiteResolver, buildLinkResolver, resolveReferences } from './site-resolver.js';
|
|
4
|
+
export type { SiteResolver, ConceptIndex, ResolvedReference } from './site-resolver.js';
|
|
5
5
|
export { createSiteIndexes } from './site-indexes.js';
|
|
6
6
|
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
package/dist/delivery/data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
|
|
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
|
-
export { createSiteResolver, buildLinkResolver } from './site-resolver.js';
|
|
5
|
+
export { createSiteResolver, buildLinkResolver, resolveReferences } from './site-resolver.js';
|
|
6
6
|
export { createSiteIndexes } from './site-indexes.js';
|
|
7
7
|
export { siteDescriptors } from './site-descriptors.js';
|
|
8
8
|
export { deriveExcerpt, wordCount } from '../content/excerpt.js';
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import type { ContentSummary, ContentEntry } from './content-index.js';
|
|
2
2
|
import type { SiteResolver } from './site-resolver.js';
|
|
3
3
|
import type { SeoMeta } from './seo.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SiteRender } from '../content/types.js';
|
|
5
5
|
import type { MediaResolve } from '../render/resolve-media.js';
|
|
6
6
|
/** Injected dependencies for the public loaders. */
|
|
7
7
|
export interface PublicRoutesDeps {
|
|
8
8
|
site: SiteResolver;
|
|
9
|
-
render:
|
|
10
|
-
stagger?: boolean;
|
|
11
|
-
resolve?: LinkResolve;
|
|
12
|
-
}) => string | Promise<string>;
|
|
9
|
+
render: SiteRender;
|
|
13
10
|
origin: string;
|
|
14
11
|
/** Site name for og:site_name and the SEO head. */
|
|
15
12
|
siteName: string;
|
|
@@ -31,6 +28,14 @@ export interface PublicRoutesDeps {
|
|
|
31
28
|
* media is off and no `heroImage` projection is derived.
|
|
32
29
|
*/
|
|
33
30
|
resolveMedia?: MediaResolve;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the site configured media on, read from `runtime.resolvedAssets.enabled`. It exists only
|
|
33
|
+
* to diagnose a forgotten wire-point: media on but no `resolveMedia` reached this factory, which
|
|
34
|
+
* renders public hero and body images as bare `media:` tokens. When true and `resolveMedia` is
|
|
35
|
+
* absent, the factory emits `media.resolver_absent` once at construction. It does not change
|
|
36
|
+
* resolution; `resolveMedia` alone still gates the hero projection.
|
|
37
|
+
*/
|
|
38
|
+
assetsEnabled?: boolean;
|
|
34
39
|
}
|
|
35
40
|
/** The archive and tag list data: summaries the template renders. */
|
|
36
41
|
export interface ListData {
|
|
@@ -8,9 +8,18 @@ import { buildSeoMeta } from './seo.js';
|
|
|
8
8
|
import { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
9
9
|
import { buildLinkResolver } from './site-resolver.js';
|
|
10
10
|
import { parseMediaToken } from '../media/reference.js';
|
|
11
|
+
import { log } from '../log/index.js';
|
|
11
12
|
/** Build the public loaders for a site's unified index. */
|
|
12
13
|
export function createPublicRoutes(deps) {
|
|
13
|
-
const { site, render, origin, siteName, description, feeds, defaultImage, resolveMedia } = deps;
|
|
14
|
+
const { site, render, origin, siteName, description, feeds, defaultImage, resolveMedia, assetsEnabled } = deps;
|
|
15
|
+
// Diagnose a forgotten wire-point: media is configured on but no resolver reached this factory, so
|
|
16
|
+
// every public hero and body `media:` token renders bare (the ecxc 0.57.0 finding). The condition
|
|
17
|
+
// is a property of the wiring, not of any one load, so it is checked once here at construction
|
|
18
|
+
// rather than per entryLoad or per image, which keeps the warning loud-once and out of the
|
|
19
|
+
// prerender hot path. Resolution is unchanged; resolveMedia alone still gates the hero projection.
|
|
20
|
+
if (assetsEnabled && !resolveMedia) {
|
|
21
|
+
log.warn('media.resolver_absent', { enabled: true });
|
|
22
|
+
}
|
|
14
23
|
/**
|
|
15
24
|
* Derive the hero projection from an entry's frontmatter, without mutating it (locked decision 5).
|
|
16
25
|
* The hero lives at the conventional `image` key as the validated nested object `{ src, alt, caption }`;
|
|
@@ -86,7 +95,21 @@ export function createPublicRoutes(deps) {
|
|
|
86
95
|
...(fields.author ? { author: fields.author } : {}),
|
|
87
96
|
...(entry.date ? { feeds } : {}),
|
|
88
97
|
});
|
|
89
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
concept: entry.concept,
|
|
100
|
+
entry,
|
|
101
|
+
html: await render({
|
|
102
|
+
body: entry.body,
|
|
103
|
+
concept: entry.concept,
|
|
104
|
+
frontmatter: entry.frontmatter,
|
|
105
|
+
resolve: buildLinkResolver(site),
|
|
106
|
+
}),
|
|
107
|
+
canonicalUrl,
|
|
108
|
+
seo,
|
|
109
|
+
newer,
|
|
110
|
+
older,
|
|
111
|
+
...(heroImage ? { heroImage } : {}),
|
|
112
|
+
};
|
|
90
113
|
}
|
|
91
114
|
/** The chronological archive for one concept: every non-draft summary, newest-first. */
|
|
92
115
|
function archiveLoad(conceptId) {
|