@glw907/cairn-cms 0.24.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +50 -37
- package/dist/auth/crypto.d.ts +0 -1
- package/dist/auth/store.d.ts +0 -1
- package/dist/auth/types.d.ts +0 -1
- package/dist/components/AdminLayout.svelte.d.ts +0 -1
- package/dist/components/ComponentForm.svelte.d.ts +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
- package/dist/components/ConceptList.svelte.d.ts +0 -1
- package/dist/components/ConfirmPage.svelte.d.ts +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts +0 -1
- package/dist/components/EditPage.svelte.d.ts +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts +0 -1
- package/dist/components/IconPicker.svelte.d.ts +0 -1
- package/dist/components/LinkPicker.svelte.d.ts +0 -1
- package/dist/components/LoginPage.svelte.d.ts +0 -1
- package/dist/components/ManageEditors.svelte.d.ts +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
- package/dist/components/NavTree.svelte.d.ts +0 -1
- package/dist/components/RenameDialog.svelte.d.ts +0 -1
- package/dist/components/index.d.ts +0 -1
- package/dist/components/link-completion.d.ts +0 -1
- package/dist/components/markdown-format.d.ts +0 -1
- package/dist/content/adapter.d.ts +0 -1
- package/dist/content/compose.d.ts +15 -5
- package/dist/content/compose.js +9 -5
- package/dist/content/concepts.d.ts +7 -1
- package/dist/content/concepts.js +49 -1
- package/dist/content/frontmatter.d.ts +0 -1
- package/dist/content/identity.d.ts +23 -0
- package/dist/content/identity.js +43 -0
- package/dist/content/ids.d.ts +0 -1
- package/dist/content/links.d.ts +0 -1
- package/dist/content/manifest.d.ts +23 -5
- package/dist/content/manifest.js +55 -32
- package/dist/content/permalink.d.ts +0 -1
- package/dist/content/schema.d.ts +0 -1
- package/dist/content/types.d.ts +0 -1
- package/dist/content/validate.d.ts +4 -2
- package/dist/content/validate.js +4 -1
- package/dist/delivery/CairnHead.svelte.d.ts +0 -1
- package/dist/delivery/content-index.d.ts +4 -1
- package/dist/delivery/content-index.js +8 -25
- package/dist/delivery/data.d.ts +23 -0
- package/dist/delivery/data.js +18 -0
- package/dist/delivery/excerpt.d.ts +0 -1
- package/dist/delivery/feeds.d.ts +0 -1
- package/dist/delivery/head.d.ts +0 -1
- package/dist/delivery/index.d.ts +1 -24
- package/dist/delivery/index.js +5 -20
- package/dist/delivery/json-ld.d.ts +0 -1
- package/dist/delivery/manifest.d.ts +0 -1
- package/dist/delivery/paginate.d.ts +0 -1
- package/dist/delivery/responses.d.ts +0 -1
- package/dist/delivery/robots.d.ts +0 -1
- package/dist/delivery/seo-fields.d.ts +0 -1
- package/dist/delivery/seo.d.ts +0 -1
- package/dist/delivery/site-descriptors.d.ts +0 -1
- package/dist/delivery/site-descriptors.js +5 -6
- package/dist/delivery/site-index.d.ts +0 -1
- package/dist/delivery/site-indexes.d.ts +0 -1
- package/dist/delivery/sitemap.d.ts +0 -1
- package/dist/email.d.ts +0 -1
- package/dist/env.d.ts +0 -1
- package/dist/github/credentials.d.ts +0 -1
- package/dist/github/repo.d.ts +0 -1
- package/dist/github/signing.d.ts +0 -1
- package/dist/github/types.d.ts +0 -1
- package/dist/index.d.ts +4 -30
- package/dist/index.js +2 -24
- package/dist/nav/site-config.d.ts +0 -1
- package/dist/render/component-grammar.d.ts +0 -1
- package/dist/render/component-insert.d.ts +0 -1
- package/dist/render/component-reference.d.ts +0 -1
- package/dist/render/component-validate.d.ts +0 -1
- package/dist/render/glyph.d.ts +0 -1
- package/dist/render/index.d.ts +0 -1
- package/dist/render/pipeline.d.ts +2 -3
- package/dist/render/pipeline.js +7 -2
- package/dist/render/registry.d.ts +0 -1
- package/dist/render/rehype-dispatch.d.ts +0 -1
- package/dist/render/remark-directives.d.ts +0 -1
- package/dist/render/resolve-links.d.ts +0 -1
- package/dist/render/sanitize-schema.d.ts +14 -1
- package/dist/render/sanitize-schema.js +96 -0
- package/dist/sveltekit/auth-routes.d.ts +0 -1
- package/dist/sveltekit/content-routes.d.ts +0 -1
- package/dist/sveltekit/editors-routes.d.ts +0 -1
- package/dist/sveltekit/guard.d.ts +0 -1
- package/dist/sveltekit/health.d.ts +0 -1
- package/dist/sveltekit/index.d.ts +1 -3
- package/dist/sveltekit/index.js +0 -1
- package/dist/sveltekit/nav-routes.d.ts +0 -1
- package/dist/sveltekit/public-routes.d.ts +0 -1
- package/dist/sveltekit/types.d.ts +0 -1
- package/dist/vite/bin.d.ts +2 -0
- package/dist/vite/bin.js +9 -0
- package/dist/vite/index.d.ts +32 -0
- package/dist/vite/index.js +178 -0
- package/package.json +22 -4
- package/src/lib/content/compose.ts +19 -10
- package/src/lib/content/concepts.ts +61 -1
- package/src/lib/content/identity.ts +60 -0
- package/src/lib/content/manifest.ts +69 -34
- package/src/lib/content/validate.ts +4 -1
- package/src/lib/delivery/content-index.ts +12 -27
- package/src/lib/delivery/data.ts +26 -0
- package/src/lib/delivery/index.ts +5 -28
- package/src/lib/delivery/site-descriptors.ts +5 -6
- package/src/lib/index.ts +4 -57
- package/src/lib/render/pipeline.ts +9 -3
- package/src/lib/render/sanitize-schema.ts +97 -0
- package/src/lib/sveltekit/index.ts +2 -8
- package/src/lib/vite/bin.ts +10 -0
- package/src/lib/vite/index.ts +213 -0
- package/dist/auth/crypto.d.ts.map +0 -1
- package/dist/auth/store.d.ts.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
- package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
- package/dist/components/ConceptList.svelte.d.ts.map +0 -1
- package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
- package/dist/components/EditPage.svelte.d.ts.map +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
- package/dist/components/IconPicker.svelte.d.ts.map +0 -1
- package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
- package/dist/components/LoginPage.svelte.d.ts.map +0 -1
- package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
- package/dist/components/NavTree.svelte.d.ts.map +0 -1
- package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/link-completion.d.ts.map +0 -1
- package/dist/components/markdown-format.d.ts.map +0 -1
- package/dist/content/adapter.d.ts.map +0 -1
- package/dist/content/compose.d.ts.map +0 -1
- package/dist/content/concepts.d.ts.map +0 -1
- package/dist/content/frontmatter.d.ts.map +0 -1
- package/dist/content/ids.d.ts.map +0 -1
- package/dist/content/links.d.ts.map +0 -1
- package/dist/content/manifest.d.ts.map +0 -1
- package/dist/content/permalink.d.ts.map +0 -1
- package/dist/content/schema.d.ts.map +0 -1
- package/dist/content/types.d.ts.map +0 -1
- package/dist/content/validate.d.ts.map +0 -1
- package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
- package/dist/delivery/content-index.d.ts.map +0 -1
- package/dist/delivery/excerpt.d.ts.map +0 -1
- package/dist/delivery/feeds.d.ts.map +0 -1
- package/dist/delivery/head.d.ts.map +0 -1
- package/dist/delivery/index.d.ts.map +0 -1
- package/dist/delivery/json-ld.d.ts.map +0 -1
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/paginate.d.ts.map +0 -1
- package/dist/delivery/responses.d.ts.map +0 -1
- package/dist/delivery/robots.d.ts.map +0 -1
- package/dist/delivery/seo-fields.d.ts.map +0 -1
- package/dist/delivery/seo.d.ts.map +0 -1
- package/dist/delivery/site-descriptors.d.ts.map +0 -1
- package/dist/delivery/site-index.d.ts.map +0 -1
- package/dist/delivery/site-indexes.d.ts.map +0 -1
- package/dist/delivery/sitemap.d.ts.map +0 -1
- package/dist/email.d.ts.map +0 -1
- package/dist/env.d.ts.map +0 -1
- package/dist/github/credentials.d.ts.map +0 -1
- package/dist/github/repo.d.ts.map +0 -1
- package/dist/github/signing.d.ts.map +0 -1
- package/dist/github/types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/nav/site-config.d.ts.map +0 -1
- package/dist/render/component-grammar.d.ts.map +0 -1
- package/dist/render/component-insert.d.ts.map +0 -1
- package/dist/render/component-reference.d.ts.map +0 -1
- package/dist/render/component-validate.d.ts.map +0 -1
- package/dist/render/glyph.d.ts.map +0 -1
- package/dist/render/index.d.ts.map +0 -1
- package/dist/render/pipeline.d.ts.map +0 -1
- package/dist/render/registry.d.ts.map +0 -1
- package/dist/render/rehype-dispatch.d.ts.map +0 -1
- package/dist/render/remark-directives.d.ts.map +0 -1
- package/dist/render/resolve-links.d.ts.map +0 -1
- package/dist/render/sanitize-schema.d.ts.map +0 -1
- package/dist/sveltekit/auth-routes.d.ts.map +0 -1
- package/dist/sveltekit/content-routes.d.ts.map +0 -1
- package/dist/sveltekit/editors-routes.d.ts.map +0 -1
- package/dist/sveltekit/guard.d.ts.map +0 -1
- package/dist/sveltekit/health.d.ts.map +0 -1
- package/dist/sveltekit/index.d.ts.map +0 -1
- package/dist/sveltekit/nav-routes.d.ts.map +0 -1
- package/dist/sveltekit/public-routes.d.ts.map +0 -1
- package/dist/sveltekit/types.d.ts.map +0 -1
|
@@ -3,18 +3,27 @@
|
|
|
3
3
|
// the same way and contributes the same kinds of things: nav entries, route logic,
|
|
4
4
|
// concepts, components, field types, and save hooks. Shaped now so the extension contract
|
|
5
5
|
// is additive later.
|
|
6
|
-
import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig,
|
|
7
|
-
import {
|
|
6
|
+
import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig, FieldTypeDef } from './types.js';
|
|
7
|
+
import { resolveConcepts } from './concepts.js';
|
|
8
|
+
import type { SiteConfig } from '../nav/site-config.js';
|
|
9
|
+
|
|
10
|
+
/** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
11
|
+
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
12
|
+
* adapter's concepts. */
|
|
13
|
+
export interface ComposeInput {
|
|
14
|
+
adapter: CairnAdapter;
|
|
15
|
+
siteConfig: SiteConfig;
|
|
16
|
+
extensions?: CairnExtension[];
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
11
|
-
*
|
|
20
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
|
|
21
|
+
* is derived from the site config, the same source the delivery path uses, so the runtime and
|
|
22
|
+
* delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
|
|
23
|
+
* (seam 4) passes through untouched.
|
|
12
24
|
*/
|
|
13
|
-
export function composeRuntime(
|
|
14
|
-
|
|
15
|
-
extensions: CairnExtension[] = [],
|
|
16
|
-
urlPolicy: Record<string, ConceptUrlPolicy | undefined> = {},
|
|
17
|
-
): CairnRuntime {
|
|
25
|
+
export function composeRuntime({ adapter, siteConfig, extensions = [] }: ComposeInput): CairnRuntime {
|
|
26
|
+
if (!siteConfig) throw new Error('composeRuntime needs a site config to derive the URL policy');
|
|
18
27
|
const content: Record<string, ConceptConfig | undefined> = { ...adapter.content };
|
|
19
28
|
const adminPanels: AdminPanel[] = [];
|
|
20
29
|
const fieldTypes: FieldTypeDef[] = [];
|
|
@@ -27,7 +36,7 @@ export function composeRuntime(
|
|
|
27
36
|
}
|
|
28
37
|
return {
|
|
29
38
|
siteName: adapter.siteName,
|
|
30
|
-
concepts:
|
|
39
|
+
concepts: resolveConcepts(content, siteConfig),
|
|
31
40
|
backend: adapter.backend,
|
|
32
41
|
sender: adapter.sender,
|
|
33
42
|
render: adapter.render,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// future Fragments concept attaches by adding one key under `content` and one routing
|
|
5
5
|
// entry, with no reshape here.
|
|
6
6
|
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
7
|
+
import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
@@ -28,6 +29,43 @@ function defaultPermalink(id: string): string {
|
|
|
28
29
|
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Permalink tokens the resolver understands. */
|
|
33
|
+
const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
|
|
34
|
+
/** The date-bearing tokens; valid only for a dated concept. */
|
|
35
|
+
const DATE_TOKENS = new Set(['year', 'month', 'day']);
|
|
36
|
+
/** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
|
|
37
|
+
const DATE_PREFIXES = new Set<string>(['year', 'month', 'day']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
|
|
41
|
+
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
42
|
+
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
43
|
+
*/
|
|
44
|
+
function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void {
|
|
45
|
+
if (policy.permalink !== undefined) {
|
|
46
|
+
const pattern = policy.permalink;
|
|
47
|
+
if (!pattern.startsWith('/')) {
|
|
48
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
|
|
49
|
+
}
|
|
50
|
+
for (const match of pattern.matchAll(/:(\w+)/g)) {
|
|
51
|
+
const token = match[1];
|
|
52
|
+
if (!KNOWN_TOKENS.has(token)) {
|
|
53
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
|
|
54
|
+
}
|
|
55
|
+
if (DATE_TOKENS.has(token) && !dated) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
/**
|
|
32
70
|
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
33
71
|
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
@@ -41,6 +79,14 @@ export function normalizeConcepts(
|
|
|
41
79
|
routing: Readonly<Record<string, RoutingRule>> = CONCEPT_ROUTING,
|
|
42
80
|
): ConceptDescriptor[] {
|
|
43
81
|
const descriptors: ConceptDescriptor[] = [];
|
|
82
|
+
const declaredConcepts = new Set(
|
|
83
|
+
Object.keys(content).filter((key) => content[key] !== undefined),
|
|
84
|
+
);
|
|
85
|
+
for (const key of Object.keys(urlPolicy)) {
|
|
86
|
+
if (!declaredConcepts.has(key)) {
|
|
87
|
+
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
44
90
|
for (const [id, config] of Object.entries(content)) {
|
|
45
91
|
if (!config) continue;
|
|
46
92
|
const summaryFields = config.summaryFields ?? [];
|
|
@@ -51,12 +97,14 @@ export function normalizeConcepts(
|
|
|
51
97
|
`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`,
|
|
52
98
|
);
|
|
53
99
|
}
|
|
100
|
+
const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
|
|
54
101
|
const policy = urlPolicy[id] ?? {};
|
|
102
|
+
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
55
103
|
descriptors.push({
|
|
56
104
|
id,
|
|
57
105
|
label: config.label ?? defaultLabel(id),
|
|
58
106
|
dir: config.dir,
|
|
59
|
-
routing:
|
|
107
|
+
routing: conceptRouting,
|
|
60
108
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
61
109
|
datePrefix: policy.datePrefix ?? 'day',
|
|
62
110
|
fields: config.schema.fields,
|
|
@@ -67,6 +115,18 @@ export function normalizeConcepts(
|
|
|
67
115
|
return descriptors;
|
|
68
116
|
}
|
|
69
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
120
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
121
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveConcepts(
|
|
124
|
+
content: Record<string, ConceptConfig | undefined>,
|
|
125
|
+
siteConfig: SiteConfig,
|
|
126
|
+
): ConceptDescriptor[] {
|
|
127
|
+
return normalizeConcepts(content, urlPolicyFrom(siteConfig));
|
|
128
|
+
}
|
|
129
|
+
|
|
70
130
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
71
131
|
export function findConcept(
|
|
72
132
|
concepts: ConceptDescriptor[],
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// cairn-cms: a content entry's URL identity in one place (engine-hardening pass 3). The id, the
|
|
2
|
+
// slug, the date, and the permalink are computed here, so the content index and the manifest cannot
|
|
3
|
+
// drift on what an entry's URL is. A cairn: link resolves through the manifest in the admin preview
|
|
4
|
+
// and through the content index in the public build, so the two must agree by construction.
|
|
5
|
+
import { idFromFilename, slugFromId } from './ids.js';
|
|
6
|
+
import { permalink } from './permalink.js';
|
|
7
|
+
import type { ConceptDescriptor } from './types.js';
|
|
8
|
+
|
|
9
|
+
/** A content entry's resolved URL identity. */
|
|
10
|
+
export interface EntryIdentity {
|
|
11
|
+
id: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
date?: string;
|
|
14
|
+
permalink: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The basename of a glob path: the segment after the last slash, or the whole path. */
|
|
18
|
+
function basename(path: string): string {
|
|
19
|
+
const slash = path.lastIndexOf('/');
|
|
20
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
24
|
+
export function asString(value: unknown): string | undefined {
|
|
25
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
29
|
+
export function asDate(value: unknown): string | undefined {
|
|
30
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
31
|
+
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Tags as an array, empty when the file declares none. */
|
|
36
|
+
export function asTags(value: unknown): string[] {
|
|
37
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
41
|
+
export function entryId(path: string): string {
|
|
42
|
+
return idFromFilename(basename(path));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
47
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
48
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
49
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
50
|
+
*/
|
|
51
|
+
export function entryIdentity(
|
|
52
|
+
descriptor: ConceptDescriptor,
|
|
53
|
+
path: string,
|
|
54
|
+
frontmatter: Record<string, unknown>,
|
|
55
|
+
): EntryIdentity {
|
|
56
|
+
const id = entryId(path);
|
|
57
|
+
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
58
|
+
const date = asDate(frontmatter.date);
|
|
59
|
+
return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
|
|
60
|
+
}
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// code reads the content graph without an N+1 GitHub crawl. The build regenerates and verifies
|
|
4
4
|
// it; the save path patches one entry and commits it with the content in one commit. Each entry
|
|
5
5
|
// carries its identity and its outbound cairn: edges, so the manifest is the link graph.
|
|
6
|
-
import { idFromFilename, slugFromId } from './ids.js';
|
|
7
6
|
import { parseMarkdown } from './frontmatter.js';
|
|
8
|
-
import {
|
|
7
|
+
import { entryIdentity, asString } from './identity.js';
|
|
9
8
|
import { extractCairnLinks, type CairnRef, type LinkResolve } from './links.js';
|
|
10
9
|
import type { ConceptDescriptor } from './types.js';
|
|
11
10
|
|
|
@@ -36,38 +35,18 @@ export interface LinkTarget {
|
|
|
36
35
|
draft: boolean;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Mirror content-index's frontmatter coercion: a present non-empty string, else undefined. */
|
|
45
|
-
function asString(value: unknown): string | undefined {
|
|
46
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Mirror content-index's date coercion: an unquoted YAML date is a JS Date, a string is sliced. */
|
|
50
|
-
function asDate(value: unknown): string | undefined {
|
|
51
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
52
|
-
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Build one manifest entry from a content file. Drafts are included and flagged. */
|
|
38
|
+
/** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
|
|
39
|
+
* permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
|
|
40
|
+
* one URL whether the admin preview reads the manifest or the public build reads the content index. */
|
|
57
41
|
export function manifestEntryFromFile(descriptor: ConceptDescriptor, file: { path: string; raw: string }): ManifestEntry {
|
|
58
|
-
const id = idFromFilename(basename(file.path));
|
|
59
|
-
// Use the same slug rule content-index uses, so the manifest's permalink for an entry always
|
|
60
|
-
// equals content-index's permalink for it. A cairn link must resolve to one URL whether the
|
|
61
|
-
// admin preview reads the manifest or the public build reads the content index.
|
|
62
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
63
42
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
64
|
-
const date =
|
|
43
|
+
const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
|
|
65
44
|
return {
|
|
66
45
|
id,
|
|
67
46
|
concept: descriptor.id,
|
|
68
47
|
title: asString(frontmatter.title) ?? id,
|
|
69
48
|
date,
|
|
70
|
-
permalink
|
|
49
|
+
permalink,
|
|
71
50
|
draft: frontmatter.draft === true,
|
|
72
51
|
links: extractCairnLinks(body),
|
|
73
52
|
};
|
|
@@ -140,15 +119,71 @@ export function parseManifest(raw: string): Manifest {
|
|
|
140
119
|
return { version: 1, entries: obj.entries as ManifestEntry[] };
|
|
141
120
|
}
|
|
142
121
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
122
|
+
/** A changed entry and the fields that differ between the built and committed manifests. */
|
|
123
|
+
export interface ManifestEntryDiff {
|
|
124
|
+
concept: string;
|
|
125
|
+
id: string;
|
|
126
|
+
fields: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The drift between a freshly built manifest and the committed one, keyed by concept+id. */
|
|
130
|
+
export interface ManifestDiff {
|
|
131
|
+
added: ManifestEntry[];
|
|
132
|
+
removed: ManifestEntry[];
|
|
133
|
+
changed: ManifestEntryDiff[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const keyOf = (e: ManifestEntry) => `${e.concept}/${e.id}`;
|
|
137
|
+
|
|
138
|
+
/** Compare a built manifest against a committed one, keyed by concept+id (the same identity
|
|
139
|
+
* upsertEntry and removeEntry use). A changed entry names the fields that differ. Pure, so it is
|
|
140
|
+
* unit-tested apart from any build. */
|
|
141
|
+
export function diffManifests(built: Manifest, committed: Manifest): ManifestDiff {
|
|
142
|
+
const builtByKey = new Map(built.entries.map((e) => [keyOf(e), e]));
|
|
143
|
+
const committedByKey = new Map(committed.entries.map((e) => [keyOf(e), e]));
|
|
144
|
+
const added = built.entries.filter((e) => !committedByKey.has(keyOf(e)));
|
|
145
|
+
const removed = committed.entries.filter((e) => !builtByKey.has(keyOf(e)));
|
|
146
|
+
const changed: ManifestEntryDiff[] = [];
|
|
147
|
+
for (const b of built.entries) {
|
|
148
|
+
const c = committedByKey.get(keyOf(b));
|
|
149
|
+
if (!c) continue;
|
|
150
|
+
// ManifestEntry has no index signature, so read its keys through an unknown-cast record.
|
|
151
|
+
const br = b as unknown as Record<string, unknown>;
|
|
152
|
+
const cr = c as unknown as Record<string, unknown>;
|
|
153
|
+
const fields = [...new Set([...Object.keys(b), ...Object.keys(c)])].filter(
|
|
154
|
+
(k) => JSON.stringify(br[k]) !== JSON.stringify(cr[k]),
|
|
150
155
|
);
|
|
156
|
+
if (fields.length > 0) changed.push({ concept: b.concept, id: b.id, fields });
|
|
151
157
|
}
|
|
158
|
+
return { added, removed, changed };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Format a diff into a short human-readable block for a build error. */
|
|
162
|
+
function formatDiff(d: ManifestDiff): string {
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
for (const e of d.added) lines.push(` + ${keyOf(e)}`);
|
|
165
|
+
for (const e of d.removed) lines.push(` - ${keyOf(e)}`);
|
|
166
|
+
for (const e of d.changed) lines.push(` ~ ${e.concept}/${e.id} (${e.fields.join(', ')})`);
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Throw if the committed manifest drifts from what the corpus says. The canonical serialized form
|
|
171
|
+
* is the fast-path equality guard, so semantic equality never spuriously fails. On a mismatch the
|
|
172
|
+
* error names the added, removed, and changed entries, so a raw-git content edit that leaves the
|
|
173
|
+
* committed manifest stale fails the build loudly with what drifted. */
|
|
174
|
+
export function verifyManifest(built: Manifest, committedRaw: string): void {
|
|
175
|
+
const builtRaw = serializeManifest(built);
|
|
176
|
+
if (committedRaw === builtRaw) return;
|
|
177
|
+
// Diff the canonical built form, not the raw one. serializeManifest sorts each entry's links, so a
|
|
178
|
+
// build whose links are in extraction order would otherwise report a false (links) drift for an
|
|
179
|
+
// entry whose link set is identical and only the order differs. Reuse the serialized form so both
|
|
180
|
+
// sides are canonical.
|
|
181
|
+
const diff = diffManifests(parseManifest(builtRaw), parseManifest(committedRaw));
|
|
182
|
+
throw new Error(
|
|
183
|
+
'content manifest is stale: the committed file does not match the corpus.\n' +
|
|
184
|
+
formatDiff(diff) +
|
|
185
|
+
'\nRegenerate it (npm run cairn:manifest) and commit the result.',
|
|
186
|
+
);
|
|
152
187
|
}
|
|
153
188
|
|
|
154
189
|
/** Replace the entry with the same concept and id, or add it. Order does not matter, since
|
|
@@ -9,7 +9,10 @@ import { dateInputValue, isCalendarDate } from './frontmatter.js';
|
|
|
9
9
|
* Validate raw frontmatter against a field list. Required text and date fields must be
|
|
10
10
|
* non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
|
|
11
11
|
* and an unchecked one is omitted; a present tag field coerces to a string array and an empty
|
|
12
|
-
* one is omitted
|
|
12
|
+
* one is omitted, so validated data carries no key for an absent tag field (`tags` or `freetags`).
|
|
13
|
+
* The delivery read model
|
|
14
|
+
* (`ContentSummary.tags`) fills that case with an empty array; the two layers differ on purpose.
|
|
15
|
+
* An empty optional text or date field is omitted, so the normalized data
|
|
13
16
|
* carries only meaningful values and committed frontmatter stays minimal. Returns the
|
|
14
17
|
* normalized data, or field-keyed errors when any required field is empty.
|
|
15
18
|
*
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// returns cheap plain-data summaries plus an on-demand detail lookup. It is concept-generic:
|
|
4
4
|
// every operation reads the descriptor and its routing rule, never a hardcoded concept id.
|
|
5
5
|
import { parseMarkdown } from '../content/frontmatter.js';
|
|
6
|
-
import {
|
|
7
|
-
import { permalink } from '../content/permalink.js';
|
|
6
|
+
import { entryId, entryIdentity, asDate, asString, asTags } from '../content/identity.js';
|
|
8
7
|
import { deriveExcerpt, wordCount } from './excerpt.js';
|
|
9
8
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
10
9
|
|
|
@@ -25,6 +24,10 @@ export interface ContentSummary {
|
|
|
25
24
|
title: string;
|
|
26
25
|
date?: string;
|
|
27
26
|
updated?: string;
|
|
27
|
+
/** The entry's tags, always present as an array and empty when the file declares none. This is the
|
|
28
|
+
* read-model normalization. It differs on purpose from the validated `frontmatter.tags`, which the
|
|
29
|
+
* validator omits when empty, so a published file carries no `tags: []` noise. Read `tags` here for
|
|
30
|
+
* a list; read `frontmatter.tags` only when you need the validated, possibly-absent value. */
|
|
28
31
|
tags: string[];
|
|
29
32
|
excerpt: string;
|
|
30
33
|
wordCount: number;
|
|
@@ -66,25 +69,6 @@ export function fromGlob(record: Record<string, string>): RawFile[] {
|
|
|
66
69
|
return Object.entries(record).map(([path, raw]) => ({ path, raw }));
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
function basename(path: string): string {
|
|
70
|
-
const slash = path.lastIndexOf('/');
|
|
71
|
-
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function asString(value: unknown): string | undefined {
|
|
75
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function asDate(value: unknown): string | undefined {
|
|
79
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
80
|
-
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function asTags(value: unknown): string[] {
|
|
85
|
-
return Array.isArray(value) ? value.map(String) : [];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
72
|
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
89
73
|
export function createContentIndex<F = Record<string, unknown>>(
|
|
90
74
|
files: RawFile[],
|
|
@@ -93,18 +77,19 @@ export function createContentIndex<F = Record<string, unknown>>(
|
|
|
93
77
|
const problems: ContentProblem[] = [];
|
|
94
78
|
const entries: ContentEntry<F>[] = [];
|
|
95
79
|
for (const file of files) {
|
|
96
|
-
const id = idFromFilename(basename(file.path));
|
|
97
|
-
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
98
80
|
const { frontmatter: raw, body } = parseMarkdown(file.raw);
|
|
99
|
-
const
|
|
81
|
+
const id = entryId(file.path);
|
|
100
82
|
const draft = raw.draft === true;
|
|
101
|
-
// Validate
|
|
102
|
-
//
|
|
83
|
+
// Validate before resolving the permalink. A date-token permalink throws on an entry with no
|
|
84
|
+
// valid date; the validate gate records that as a content problem rather than aborting the whole
|
|
85
|
+
// index build, so one bad entry degrades to a skip, not a crash. A failure is also excluded from
|
|
86
|
+
// the typed read, so every readable entry's frontmatter is the validator's normalized output.
|
|
103
87
|
const result = descriptor.validate(raw, body);
|
|
104
88
|
if (!result.ok) {
|
|
105
89
|
problems.push({ id, draft, errors: result.errors });
|
|
106
90
|
continue;
|
|
107
91
|
}
|
|
92
|
+
const { slug, date, permalink } = entryIdentity(descriptor, file.path, raw);
|
|
108
93
|
const summaryFieldValues: Record<string, unknown> = {};
|
|
109
94
|
for (const key of descriptor.summaryFields) {
|
|
110
95
|
if (key in result.data) summaryFieldValues[key] = result.data[key];
|
|
@@ -113,7 +98,7 @@ export function createContentIndex<F = Record<string, unknown>>(
|
|
|
113
98
|
concept: descriptor.id,
|
|
114
99
|
id,
|
|
115
100
|
slug,
|
|
116
|
-
permalink
|
|
101
|
+
permalink,
|
|
117
102
|
title: asString(raw.title) ?? id,
|
|
118
103
|
date,
|
|
119
104
|
updated: asDate(raw.updated),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// cairn-cms: the node-safe delivery data surface (@glw907/cairn-cms/delivery/data). The pure corpus
|
|
2
|
+
// projections a SvelteKit site or a plain-Node tool reads, with no @sveltejs/kit and no .svelte in
|
|
3
|
+
// the graph. The full ./delivery barrel re-exports this and adds the route loaders.
|
|
4
|
+
export { createContentIndex, fromGlob } from './content-index.js';
|
|
5
|
+
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
6
|
+
export { createSiteIndex } from './site-index.js';
|
|
7
|
+
export type { SiteIndex, ConceptIndex } from './site-index.js';
|
|
8
|
+
export { createSiteIndexes } from './site-indexes.js';
|
|
9
|
+
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
10
|
+
export { siteDescriptors } from './site-descriptors.js';
|
|
11
|
+
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
12
|
+
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
13
|
+
export type { FeedChannel, FeedItem } from './feeds.js';
|
|
14
|
+
export { buildSitemap } from './sitemap.js';
|
|
15
|
+
export type { SitemapUrl } from './sitemap.js';
|
|
16
|
+
export { buildRobots } from './robots.js';
|
|
17
|
+
export { buildSeoMeta } from './seo.js';
|
|
18
|
+
export type { SeoInput, SeoMeta } from './seo.js';
|
|
19
|
+
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
20
|
+
export type { SeoFields } from './seo-fields.js';
|
|
21
|
+
export { paginate } from './paginate.js';
|
|
22
|
+
export type { Page } from './paginate.js';
|
|
23
|
+
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
24
|
+
export { jsonLdScript } from './json-ld.js';
|
|
25
|
+
export { permalink } from '../content/permalink.js';
|
|
26
|
+
export { buildSiteManifest, buildLinkResolver } from './manifest.js';
|
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
// cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
export { createContentIndex, fromGlob } from './content-index.js';
|
|
7
|
-
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem } from './content-index.js';
|
|
8
|
-
export { createSiteIndex } from './site-index.js';
|
|
9
|
-
export type { SiteIndex, ConceptIndex } from './site-index.js';
|
|
10
|
-
export { createSiteIndexes } from './site-indexes.js';
|
|
11
|
-
export type { SiteIndexes, SiteGlobs } from './site-indexes.js';
|
|
12
|
-
export { siteDescriptors } from './site-descriptors.js';
|
|
13
|
-
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
14
|
-
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
15
|
-
export type { FeedChannel, FeedItem } from './feeds.js';
|
|
16
|
-
export { buildSitemap } from './sitemap.js';
|
|
17
|
-
export type { SitemapUrl } from './sitemap.js';
|
|
18
|
-
export { buildRobots } from './robots.js';
|
|
19
|
-
export { buildSeoMeta } from './seo.js';
|
|
20
|
-
export type { SeoInput, SeoMeta } from './seo.js';
|
|
21
|
-
export { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
22
|
-
export type { SeoFields } from './seo-fields.js';
|
|
23
|
-
export { paginate } from './paginate.js';
|
|
24
|
-
export type { Page } from './paginate.js';
|
|
25
|
-
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
26
|
-
export { jsonLdScript } from './json-ld.js';
|
|
27
|
-
export { permalink } from '../content/permalink.js';
|
|
28
|
-
export { buildSiteManifest, buildLinkResolver } from './manifest.js';
|
|
1
|
+
// cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The node-safe data surface
|
|
2
|
+
// (re-exported from ./delivery/data) plus the SvelteKit catch-all route loaders. The head component
|
|
3
|
+
// lives at ./delivery/head. Importing this pulls @sveltejs/kit through the route loaders, so a
|
|
4
|
+
// plain-Node tool imports from ./delivery/data instead.
|
|
5
|
+
export * from './data.js';
|
|
29
6
|
export { createPublicRoutes } from '../sveltekit/public-routes.js';
|
|
30
7
|
export type {
|
|
31
8
|
PublicRoutesDeps,
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import {
|
|
5
|
-
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
1
|
+
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept descriptors
|
|
2
|
+
// the admin runtime uses; this delegates to the shared resolveConcepts so the pairing is one path, not
|
|
3
|
+
// tribal knowledge. The YAML URL policy stays the single source of truth.
|
|
4
|
+
import { resolveConcepts } from '../content/concepts.js';
|
|
6
5
|
import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
|
|
7
6
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
8
7
|
|
|
9
8
|
/** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
|
|
10
9
|
export function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[] {
|
|
11
|
-
return
|
|
10
|
+
return resolveConcepts(adapter.content, siteConfig);
|
|
12
11
|
}
|
package/src/lib/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type {
|
|
|
31
31
|
} from './content/types.js';
|
|
32
32
|
export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
|
|
33
33
|
export { composeRuntime } from './content/compose.js';
|
|
34
|
+
export type { ComposeInput } from './content/compose.js';
|
|
34
35
|
export {
|
|
35
36
|
frontmatterFromForm,
|
|
36
37
|
dateInputValue,
|
|
@@ -59,13 +60,14 @@ export {
|
|
|
59
60
|
parseManifest,
|
|
60
61
|
emptyManifest,
|
|
61
62
|
verifyManifest,
|
|
63
|
+
diffManifests,
|
|
62
64
|
upsertEntry,
|
|
63
65
|
removeEntry,
|
|
64
66
|
manifestEntryFromFile,
|
|
65
67
|
manifestLinkResolver,
|
|
66
68
|
inboundLinks,
|
|
67
69
|
} from './content/manifest.js';
|
|
68
|
-
export type { Manifest, ManifestEntry, LinkTarget, InboundLink } from './content/manifest.js';
|
|
70
|
+
export type { Manifest, ManifestEntry, ManifestDiff, ManifestEntryDiff, LinkTarget, InboundLink } from './content/manifest.js';
|
|
69
71
|
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
70
72
|
export { defineRegistry, emptyValues } from './render/registry.js';
|
|
71
73
|
export type {
|
|
@@ -86,15 +88,7 @@ export type { ReferenceOptions } from './render/component-reference.js';
|
|
|
86
88
|
export { glyph } from './render/glyph.js';
|
|
87
89
|
export type { IconSet } from './render/glyph.js';
|
|
88
90
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
89
|
-
export {
|
|
90
|
-
rehypeDispatch,
|
|
91
|
-
isElement,
|
|
92
|
-
strProp,
|
|
93
|
-
iconSpan,
|
|
94
|
-
cardShell,
|
|
95
|
-
headRow,
|
|
96
|
-
markFirstList,
|
|
97
|
-
} from './render/rehype-dispatch.js';
|
|
91
|
+
export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
|
|
98
92
|
export type { MakeIcon } from './render/rehype-dispatch.js';
|
|
99
93
|
export { createRenderer } from './render/pipeline.js';
|
|
100
94
|
export type { RendererOptions } from './render/pipeline.js';
|
|
@@ -102,18 +96,6 @@ export type { RendererOptions } from './render/pipeline.js';
|
|
|
102
96
|
// GitHub read-and-commit backend (Plan 03).
|
|
103
97
|
export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
|
|
104
98
|
export { CommitConflictError } from './github/types.js';
|
|
105
|
-
export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
|
|
106
|
-
export {
|
|
107
|
-
treeUrl,
|
|
108
|
-
markdownFilesIn,
|
|
109
|
-
listMarkdown,
|
|
110
|
-
contentsUrl,
|
|
111
|
-
readRaw,
|
|
112
|
-
fileSha,
|
|
113
|
-
commitFile,
|
|
114
|
-
} from './github/repo.js';
|
|
115
|
-
export { appCredentials } from './github/credentials.js';
|
|
116
|
-
export type { GithubKeyEnv } from './github/credentials.js';
|
|
117
99
|
|
|
118
100
|
// Nav tree and site-config helpers (Plan 06).
|
|
119
101
|
export {
|
|
@@ -127,38 +109,3 @@ export {
|
|
|
127
109
|
SiteConfigError,
|
|
128
110
|
} from './nav/site-config.js';
|
|
129
111
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
|
130
|
-
|
|
131
|
-
// Public content delivery (public-delivery design): the query index, syndication, and
|
|
132
|
-
// discovery surface that sites read. Pure builders plus the one permalink resolver; the
|
|
133
|
-
// SvelteKit loaders live under the /sveltekit subpath.
|
|
134
|
-
export { permalink } from './content/permalink.js';
|
|
135
|
-
export { createContentIndex, fromGlob } from './delivery/content-index.js';
|
|
136
|
-
export type {
|
|
137
|
-
RawFile,
|
|
138
|
-
ContentSummary,
|
|
139
|
-
ContentEntry,
|
|
140
|
-
ContentIndex,
|
|
141
|
-
ContentProblem,
|
|
142
|
-
} from './delivery/content-index.js';
|
|
143
|
-
export { createSiteIndex } from './delivery/site-index.js';
|
|
144
|
-
export type { SiteIndex, ConceptIndex } from './delivery/site-index.js';
|
|
145
|
-
export { createSiteIndexes } from './delivery/site-indexes.js';
|
|
146
|
-
export type { SiteIndexes, SiteGlobs } from './delivery/site-indexes.js';
|
|
147
|
-
export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
|
|
148
|
-
export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
|
|
149
|
-
export type { FeedChannel, FeedItem } from './delivery/feeds.js';
|
|
150
|
-
export { buildSitemap } from './delivery/sitemap.js';
|
|
151
|
-
export type { SitemapUrl } from './delivery/sitemap.js';
|
|
152
|
-
export { buildRobots } from './delivery/robots.js';
|
|
153
|
-
export { buildSeoMeta } from './delivery/seo.js';
|
|
154
|
-
export type { SeoInput, SeoMeta } from './delivery/seo.js';
|
|
155
|
-
export { readSeoFields, resolveImageUrl } from './delivery/seo-fields.js';
|
|
156
|
-
export type { SeoFields } from './delivery/seo-fields.js';
|
|
157
|
-
export { paginate } from './delivery/paginate.js';
|
|
158
|
-
export type { Page } from './delivery/paginate.js';
|
|
159
|
-
// Root superset of the delivery route surface: a wrong guess from root for a route loader or a
|
|
160
|
-
// response helper now resolves. The CairnHead component stays out of root so the root barrel stays
|
|
161
|
-
// node-importable for the unit suite; it resolves from @glw907/cairn-cms/delivery/head.
|
|
162
|
-
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './delivery/responses.js';
|
|
163
|
-
export { createPublicRoutes } from './sveltekit/public-routes.js';
|
|
164
|
-
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData } from './sveltekit/public-routes.js';
|