@glw907/cairn-cms 0.6.0-rc.1 → 0.7.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/dist/components/EditPage.svelte +5 -5
- package/dist/components/EditPage.svelte.d.ts +3 -1
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/content/compose.js +1 -1
- package/dist/content/concepts.d.ts.map +1 -1
- package/dist/content/concepts.js +5 -0
- package/dist/content/permalink.d.ts +11 -0
- package/dist/content/permalink.d.ts.map +1 -0
- package/dist/content/permalink.js +30 -0
- package/dist/content/types.d.ts +17 -3
- package/dist/content/types.d.ts.map +1 -1
- package/dist/delivery/content-index.d.ts +46 -0
- package/dist/delivery/content-index.d.ts.map +1 -0
- package/dist/delivery/content-index.js +84 -0
- package/dist/delivery/excerpt.d.ts +11 -0
- package/dist/delivery/excerpt.d.ts.map +1 -0
- package/dist/delivery/excerpt.js +38 -0
- package/dist/delivery/feeds.d.ts +27 -0
- package/dist/delivery/feeds.d.ts.map +1 -0
- package/dist/delivery/feeds.js +80 -0
- package/dist/delivery/paginate.d.ts +13 -0
- package/dist/delivery/paginate.d.ts.map +1 -0
- package/dist/delivery/paginate.js +20 -0
- package/dist/delivery/robots.d.ts +6 -0
- package/dist/delivery/robots.d.ts.map +1 -0
- package/dist/delivery/robots.js +10 -0
- package/dist/delivery/seo.d.ts +34 -0
- package/dist/delivery/seo.d.ts.map +1 -0
- package/dist/delivery/seo.js +46 -0
- package/dist/delivery/sitemap.d.ts +8 -0
- package/dist/delivery/sitemap.d.ts.map +1 -0
- package/dist/delivery/sitemap.js +21 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/sveltekit/index.d.ts +2 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/public-routes.d.ts +51 -0
- package/dist/sveltekit/public-routes.d.ts.map +1 -0
- package/dist/sveltekit/public-routes.js +44 -0
- package/package.json +1 -1
- package/src/lib/components/EditPage.svelte +5 -5
- package/src/lib/content/compose.ts +1 -1
- package/src/lib/content/concepts.ts +6 -0
- package/src/lib/content/permalink.ts +40 -0
- package/src/lib/content/types.ts +14 -4
- package/src/lib/delivery/content-index.ts +127 -0
- package/src/lib/delivery/excerpt.ts +41 -0
- package/src/lib/delivery/feeds.ts +112 -0
- package/src/lib/delivery/paginate.ts +32 -0
- package/src/lib/delivery/robots.ts +10 -0
- package/src/lib/delivery/seo.ts +72 -0
- package/src/lib/delivery/sitemap.ts +29 -0
- package/src/lib/index.ts +22 -0
- package/src/lib/sveltekit/index.ts +8 -0
- package/src/lib/sveltekit/public-routes.ts +81 -0
|
@@ -21,10 +21,10 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
21
21
|
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
22
22
|
preview?: unknown[];
|
|
23
23
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
24
|
-
|
|
24
|
+
render?: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
let { data, registry, preview = [],
|
|
27
|
+
let { data, registry, preview = [], render }: Props = $props();
|
|
28
28
|
|
|
29
29
|
// `body` is local editor state seeded once from the prop; it diverges as the user types.
|
|
30
30
|
// untrack() captures the initial value without subscribing to future prop changes.
|
|
@@ -48,15 +48,15 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
48
48
|
// Render the design-accurate preview as the body changes, debounced, and sanitize before the DOM.
|
|
49
49
|
// The sanitize is the one barrier between editor-authored markdown and the page (Carta is unsanitized).
|
|
50
50
|
// previewRun is a plain counter (not reactive state) used as a latest-wins guard: if a slow earlier
|
|
51
|
-
// async
|
|
51
|
+
// async render call resolves after a newer one has started, the stale result is discarded.
|
|
52
52
|
let previewRun = 0;
|
|
53
53
|
$effect(() => {
|
|
54
|
-
if (!showPreview || !
|
|
54
|
+
if (!showPreview || !render) return;
|
|
55
55
|
const md = body;
|
|
56
56
|
const run = ++previewRun;
|
|
57
57
|
const handle = setTimeout(async () => {
|
|
58
58
|
try {
|
|
59
|
-
const html = await
|
|
59
|
+
const html = await render(md);
|
|
60
60
|
const safe = await sanitizePreviewHtml(html);
|
|
61
61
|
if (run === previewRun) previewHtml = safe;
|
|
62
62
|
} catch {
|
|
@@ -10,7 +10,9 @@ interface Props {
|
|
|
10
10
|
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
11
11
|
preview?: unknown[];
|
|
12
12
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
13
|
-
|
|
13
|
+
render?: (md: string, opts?: {
|
|
14
|
+
stagger?: boolean;
|
|
15
|
+
}) => string | Promise<string>;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the Carta
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,yFAAyF;IACzF,
|
|
1
|
+
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjF;AAqJH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
package/dist/content/compose.js
CHANGED
|
@@ -22,7 +22,7 @@ export function composeRuntime(adapter, extensions = []) {
|
|
|
22
22
|
concepts: normalizeConcepts(content),
|
|
23
23
|
backend: adapter.backend,
|
|
24
24
|
sender: adapter.sender,
|
|
25
|
-
|
|
25
|
+
render: adapter.render,
|
|
26
26
|
registry: adapter.registry,
|
|
27
27
|
navMenu: adapter.navMenu,
|
|
28
28
|
assets: adapter.assets,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAGjE,CAAC;
|
|
1
|
+
{"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAGjE,CAAC;AAeF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,EAClD,OAAO,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAmB,GAC/D,iBAAiB,EAAE,CAerB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,EAAE,EAAE,MAAM,GACT,iBAAiB,GAAG,SAAS,CAE/B"}
|
package/dist/content/concepts.js
CHANGED
|
@@ -13,6 +13,10 @@ const DEFAULT_ROUTING = { routable: true, dated: false, inFeeds: false };
|
|
|
13
13
|
function defaultLabel(id) {
|
|
14
14
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
15
15
|
}
|
|
16
|
+
/** The default permalink pattern: Pages live at the root, other concepts under their id. */
|
|
17
|
+
function defaultPermalink(id) {
|
|
18
|
+
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
19
|
+
}
|
|
16
20
|
/**
|
|
17
21
|
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each declared
|
|
18
22
|
* key under `content` becomes one descriptor; an undeclared (`undefined`) concept is
|
|
@@ -29,6 +33,7 @@ export function normalizeConcepts(content, routing = CONCEPT_ROUTING) {
|
|
|
29
33
|
label: config.label ?? defaultLabel(id),
|
|
30
34
|
dir: config.dir,
|
|
31
35
|
routing: routing[id] ?? DEFAULT_ROUTING,
|
|
36
|
+
permalink: config.permalink ?? defaultPermalink(id),
|
|
32
37
|
fields: config.fields,
|
|
33
38
|
validate: config.validate,
|
|
34
39
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an entry's canonical path from its concept's permalink pattern. Throws when the
|
|
4
|
+
* pattern uses a date token and the entry has no valid date, or when a token is unknown, so
|
|
5
|
+
* a misconfiguration fails at build rather than emitting a broken path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function permalink(descriptor: ConceptDescriptor, entry: {
|
|
8
|
+
id: string;
|
|
9
|
+
date?: string;
|
|
10
|
+
}): string;
|
|
11
|
+
//# sourceMappingURL=permalink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permalink.d.ts","sourceRoot":"","sources":["../../src/lib/content/permalink.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAWpD;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,iBAAiB,EAC7B,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACnC,MAAM,CAgBR"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function pad(n) {
|
|
2
|
+
return String(n).padStart(2, '0');
|
|
3
|
+
}
|
|
4
|
+
function dateParts(date) {
|
|
5
|
+
const match = date?.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
6
|
+
return match ? { year: match[1], month: match[2], day: match[3] } : null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an entry's canonical path from its concept's permalink pattern. Throws when the
|
|
10
|
+
* pattern uses a date token and the entry has no valid date, or when a token is unknown, so
|
|
11
|
+
* a misconfiguration fails at build rather than emitting a broken path.
|
|
12
|
+
*/
|
|
13
|
+
export function permalink(descriptor, entry) {
|
|
14
|
+
return descriptor.permalink.replace(/:(\w+)/g, (_match, token) => {
|
|
15
|
+
if (token === 'slug')
|
|
16
|
+
return entry.id;
|
|
17
|
+
if (token === 'year' || token === 'month' || token === 'day') {
|
|
18
|
+
const parts = dateParts(entry.date);
|
|
19
|
+
if (!parts) {
|
|
20
|
+
throw new Error(`permalink: concept "${descriptor.id}" pattern uses :${token}, but entry "${entry.id}" has no valid date`);
|
|
21
|
+
}
|
|
22
|
+
if (token === 'year')
|
|
23
|
+
return parts.year;
|
|
24
|
+
if (token === 'month')
|
|
25
|
+
return pad(Number(parts.month));
|
|
26
|
+
return pad(Number(parts.day));
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`permalink: unknown token :${token} in pattern "${descriptor.permalink}"`);
|
|
29
|
+
});
|
|
30
|
+
}
|
package/dist/content/types.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface ConceptConfig {
|
|
|
68
68
|
fields: FrontmatterField[];
|
|
69
69
|
/** Validate submitted frontmatter before any commit. */
|
|
70
70
|
validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
|
|
71
|
+
/**
|
|
72
|
+
* Public URL pattern for this concept, a `/`-prefixed string of literal segments and the
|
|
73
|
+
* tokens `:slug`, `:year`, `:month`, `:day`. `normalizeConcepts` fills a per-concept
|
|
74
|
+
* default when omitted (`/:slug` for Pages, `/<conceptId>/:slug` otherwise). The pattern
|
|
75
|
+
* must agree with the site's filesystem route directory.
|
|
76
|
+
*/
|
|
77
|
+
permalink?: string;
|
|
71
78
|
}
|
|
72
79
|
/** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
|
|
73
80
|
export interface BackendConfig {
|
|
@@ -114,8 +121,10 @@ export interface CairnAdapter {
|
|
|
114
121
|
};
|
|
115
122
|
backend: BackendConfig;
|
|
116
123
|
sender: SenderConfig;
|
|
117
|
-
/**
|
|
118
|
-
|
|
124
|
+
/** The site's one renderer: the editor preview and every public page call it (design decision 4). */
|
|
125
|
+
render(md: string, opts?: {
|
|
126
|
+
stagger?: boolean;
|
|
127
|
+
}): string | Promise<string>;
|
|
119
128
|
/** Directive component registry; the renderer and the future palette derive from it (seam 3). */
|
|
120
129
|
registry?: ComponentRegistry;
|
|
121
130
|
navMenu?: NavMenuConfig;
|
|
@@ -143,6 +152,8 @@ export interface ConceptDescriptor {
|
|
|
143
152
|
label: string;
|
|
144
153
|
dir: string;
|
|
145
154
|
routing: RoutingRule;
|
|
155
|
+
/** The resolved permalink pattern, defaulted by `normalizeConcepts`. */
|
|
156
|
+
permalink: string;
|
|
146
157
|
fields: FrontmatterField[];
|
|
147
158
|
validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
|
|
148
159
|
}
|
|
@@ -197,7 +208,10 @@ export interface CairnRuntime {
|
|
|
197
208
|
concepts: ConceptDescriptor[];
|
|
198
209
|
backend: BackendConfig;
|
|
199
210
|
sender: SenderConfig;
|
|
200
|
-
|
|
211
|
+
/** The site's one renderer: the editor preview and every public page call it (design decision 4). */
|
|
212
|
+
render(md: string, opts?: {
|
|
213
|
+
stagger?: boolean;
|
|
214
|
+
}): string | Promise<string>;
|
|
201
215
|
registry?: ComponentRegistry;
|
|
202
216
|
navMenu?: NavMenuConfig;
|
|
203
217
|
assets?: AssetConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,0GAA0G;AAC1G,UAAU,SAAS;IACjB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gCAAgC;AAChC,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,+BAA+B;AAC/B,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,iCAAiC;AACjC,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,sCAAsC;AACtC,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,IAAI,EAAE,SAAS,CAAC;CACjB;AACD,sEAAsE;AACtE,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AACD,iEAAiE;AACjE,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,aAAa,GACb,SAAS,GACT,YAAY,GACZ,SAAS,GACT,aAAa,CAAC;AAElB;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC3C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAElD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
+
/** A raw content file before parsing: the glob key and the file's full markdown text. */
|
|
3
|
+
export interface RawFile {
|
|
4
|
+
path: string;
|
|
5
|
+
raw: string;
|
|
6
|
+
}
|
|
7
|
+
/** The cheap, plain-data view of one entry, for lists, feeds, and the sitemap. */
|
|
8
|
+
export interface ContentSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
permalink: string;
|
|
11
|
+
title: string;
|
|
12
|
+
date?: string;
|
|
13
|
+
updated?: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
excerpt: string;
|
|
16
|
+
wordCount: number;
|
|
17
|
+
draft: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The detail view: a summary plus the frontmatter and the body to render. */
|
|
20
|
+
export interface ContentEntry extends ContentSummary {
|
|
21
|
+
frontmatter: Record<string, unknown>;
|
|
22
|
+
body: string;
|
|
23
|
+
}
|
|
24
|
+
/** The per-concept query surface. */
|
|
25
|
+
export interface ContentIndex {
|
|
26
|
+
all(opts?: {
|
|
27
|
+
includeDrafts?: boolean;
|
|
28
|
+
}): ContentSummary[];
|
|
29
|
+
byId(id: string): ContentEntry | undefined;
|
|
30
|
+
byTag(tag: string, opts?: {
|
|
31
|
+
includeDrafts?: boolean;
|
|
32
|
+
}): ContentSummary[];
|
|
33
|
+
allTags(): {
|
|
34
|
+
tag: string;
|
|
35
|
+
count: number;
|
|
36
|
+
}[];
|
|
37
|
+
adjacent(id: string): {
|
|
38
|
+
newer?: ContentSummary;
|
|
39
|
+
older?: ContentSummary;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
|
|
43
|
+
export declare function fromGlob(record: Record<string, string>): RawFile[];
|
|
44
|
+
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
45
|
+
export declare function createContentIndex(files: RawFile[], descriptor: ConceptDescriptor): ContentIndex;
|
|
46
|
+
//# sourceMappingURL=content-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/content-index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IAC1D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC3C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,cAAc,EAAE,CAAC;IACzE,OAAO,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;CAC1E;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAElE;AAqBD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,iBAAiB,GAAG,YAAY,CAyDhG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// cairn-cms: the per-concept content index (public-delivery design, decisions 1 and 5). It
|
|
2
|
+
// takes raw files from a site's glob, parses them with the engine's own parseMarkdown, and
|
|
3
|
+
// returns cheap plain-data summaries plus an on-demand detail lookup. It is concept-generic:
|
|
4
|
+
// every operation reads the descriptor and its routing rule, never a hardcoded concept id.
|
|
5
|
+
import { parseMarkdown } from '../content/frontmatter.js';
|
|
6
|
+
import { idFromFilename } from '../content/ids.js';
|
|
7
|
+
import { permalink } from '../content/permalink.js';
|
|
8
|
+
import { deriveExcerpt, wordCount } from './excerpt.js';
|
|
9
|
+
/** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
|
|
10
|
+
export function fromGlob(record) {
|
|
11
|
+
return Object.entries(record).map(([path, raw]) => ({ path, raw }));
|
|
12
|
+
}
|
|
13
|
+
function basename(path) {
|
|
14
|
+
const slash = path.lastIndexOf('/');
|
|
15
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
16
|
+
}
|
|
17
|
+
function asString(value) {
|
|
18
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function asDate(value) {
|
|
21
|
+
if (value instanceof Date)
|
|
22
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function asTags(value) {
|
|
28
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
29
|
+
}
|
|
30
|
+
/** Build a concept's index from its raw files and normalized descriptor. */
|
|
31
|
+
export function createContentIndex(files, descriptor) {
|
|
32
|
+
const entries = files.map((file) => {
|
|
33
|
+
const id = idFromFilename(basename(file.path));
|
|
34
|
+
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
35
|
+
const date = asDate(frontmatter.date);
|
|
36
|
+
return {
|
|
37
|
+
id,
|
|
38
|
+
permalink: permalink(descriptor, { id, date }),
|
|
39
|
+
title: asString(frontmatter.title) ?? id,
|
|
40
|
+
date,
|
|
41
|
+
updated: asDate(frontmatter.updated),
|
|
42
|
+
tags: asTags(frontmatter.tags),
|
|
43
|
+
excerpt: deriveExcerpt(body, { description: asString(frontmatter.description) }),
|
|
44
|
+
wordCount: wordCount(body),
|
|
45
|
+
draft: frontmatter.draft === true,
|
|
46
|
+
frontmatter,
|
|
47
|
+
body,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// Dated concepts sort newest-first; undated concepts (Pages) sort by title.
|
|
51
|
+
const sorted = [...entries].sort((a, b) => descriptor.routing.dated ? (b.date ?? '').localeCompare(a.date ?? '') : a.title.localeCompare(b.title));
|
|
52
|
+
const summarize = (entry) => {
|
|
53
|
+
const { frontmatter: _frontmatter, body: _body, ...summary } = entry;
|
|
54
|
+
return summary;
|
|
55
|
+
};
|
|
56
|
+
const visible = (list, includeDrafts) => includeDrafts ? list : list.filter((entry) => !entry.draft);
|
|
57
|
+
return {
|
|
58
|
+
all: (opts = {}) => visible(sorted, opts.includeDrafts).map(summarize),
|
|
59
|
+
byId: (id) => entries.find((entry) => entry.id === id),
|
|
60
|
+
byTag: (tag, opts = {}) => visible(sorted, opts.includeDrafts)
|
|
61
|
+
.filter((entry) => entry.tags.includes(tag))
|
|
62
|
+
.map(summarize),
|
|
63
|
+
allTags: () => {
|
|
64
|
+
const counts = new Map();
|
|
65
|
+
for (const entry of sorted) {
|
|
66
|
+
if (entry.draft)
|
|
67
|
+
continue;
|
|
68
|
+
for (const tag of entry.tags)
|
|
69
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => a.tag.localeCompare(b.tag));
|
|
72
|
+
},
|
|
73
|
+
adjacent: (id) => {
|
|
74
|
+
const list = visible(sorted, false);
|
|
75
|
+
const i = list.findIndex((entry) => entry.id === id);
|
|
76
|
+
if (i < 0)
|
|
77
|
+
return {};
|
|
78
|
+
return {
|
|
79
|
+
newer: i > 0 ? summarize(list[i - 1]) : undefined,
|
|
80
|
+
older: i < list.length - 1 ? summarize(list[i + 1]) : undefined,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plain-text excerpt. Returns a trimmed frontmatter `description` when present, else the
|
|
3
|
+
* stripped body cut at a word boundary near `maxChars` (default 200) with an ellipsis.
|
|
4
|
+
*/
|
|
5
|
+
export declare function deriveExcerpt(body: string, opts?: {
|
|
6
|
+
description?: string;
|
|
7
|
+
maxChars?: number;
|
|
8
|
+
}): string;
|
|
9
|
+
/** Count words in the stripped body. */
|
|
10
|
+
export declare function wordCount(body: string): number;
|
|
11
|
+
//# sourceMappingURL=excerpt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"excerpt.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/excerpt.ts"],"names":[],"mappings":"AAmBA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAW1G;AAED,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9C"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// cairn-cms: excerpt and word count for content summaries (public-delivery design, decision
|
|
2
|
+
// 5). A light markdown strip keeps summaries cheap, so a list card, an og:description, and a
|
|
3
|
+
// summary-mode feed read one derived excerpt without a full render.
|
|
4
|
+
/** Reduce markdown to readable plain text: drop fenced code, images, and markup; unwrap inline
|
|
5
|
+
* code and links to their text; collapse whitespace. */
|
|
6
|
+
function toPlainText(md) {
|
|
7
|
+
return md
|
|
8
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
9
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
10
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
|
11
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
12
|
+
.replace(/^\s{0,3}[#>]+\s*/gm, ' ')
|
|
13
|
+
.replace(/^\s{0,3}[-*+]\s+/gm, ' ')
|
|
14
|
+
.replace(/[*_~]/g, '')
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A plain-text excerpt. Returns a trimmed frontmatter `description` when present, else the
|
|
20
|
+
* stripped body cut at a word boundary near `maxChars` (default 200) with an ellipsis.
|
|
21
|
+
*/
|
|
22
|
+
export function deriveExcerpt(body, opts = {}) {
|
|
23
|
+
const description = opts.description?.trim();
|
|
24
|
+
if (description)
|
|
25
|
+
return description;
|
|
26
|
+
const max = opts.maxChars ?? 200;
|
|
27
|
+
const text = toPlainText(body);
|
|
28
|
+
if (text.length <= max)
|
|
29
|
+
return text;
|
|
30
|
+
const cut = text.slice(0, max);
|
|
31
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
32
|
+
return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
|
|
33
|
+
}
|
|
34
|
+
/** Count words in the stripped body. */
|
|
35
|
+
export function wordCount(body) {
|
|
36
|
+
const text = toPlainText(body);
|
|
37
|
+
return text ? text.split(/\s+/).length : 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Feed channel metadata. URLs are absolute. */
|
|
2
|
+
export interface FeedChannel {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
siteUrl: string;
|
|
6
|
+
feedUrl: string;
|
|
7
|
+
language?: string;
|
|
8
|
+
author?: {
|
|
9
|
+
name: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** One feed entry. `contentHtml` carries the rendered body for a full-content feed. */
|
|
14
|
+
export interface FeedItem {
|
|
15
|
+
title: string;
|
|
16
|
+
url: string;
|
|
17
|
+
date: string;
|
|
18
|
+
updated?: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
contentHtml?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Build an RSS 2.0 document. */
|
|
24
|
+
export declare function buildRssFeed(channel: FeedChannel, items: FeedItem[]): string;
|
|
25
|
+
/** Build a JSON Feed 1.1 document. */
|
|
26
|
+
export declare function buildJsonFeed(channel: FeedChannel, items: FeedItem[]): string;
|
|
27
|
+
//# sourceMappingURL=feeds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/feeds.ts"],"names":[],"mappings":"AAKA,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAyBD,iCAAiC;AACjC,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAkC5E;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAwB7E"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// cairn-cms: RSS and JSON Feed builders (public-delivery design). Pure functions over a
|
|
2
|
+
// channel and a list of items, so they unit-test without a render or a network. The caller
|
|
3
|
+
// (a template +server.ts shim) assembles items from the content index and passes absolute
|
|
4
|
+
// URLs built from PUBLIC_ORIGIN.
|
|
5
|
+
function escapeXml(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
11
|
+
}
|
|
12
|
+
/** Make a string safe inside a CDATA section by splitting any `]]>` across two sections. */
|
|
13
|
+
function cdataSafe(value) {
|
|
14
|
+
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
15
|
+
}
|
|
16
|
+
/** Format a YYYY-MM-DD (or ISO) string as an RFC-822 date in UTC, as RSS wants. */
|
|
17
|
+
function rfc822(date) {
|
|
18
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toUTCString();
|
|
19
|
+
}
|
|
20
|
+
/** Format a YYYY-MM-DD (or ISO) string as an ISO-8601 instant in UTC. */
|
|
21
|
+
function iso(date) {
|
|
22
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toISOString();
|
|
23
|
+
}
|
|
24
|
+
/** Build an RSS 2.0 document. */
|
|
25
|
+
export function buildRssFeed(channel, items) {
|
|
26
|
+
const entries = items
|
|
27
|
+
.map((item) => {
|
|
28
|
+
const content = item.contentHtml ?? item.summary;
|
|
29
|
+
return [
|
|
30
|
+
' <item>',
|
|
31
|
+
` <title>${escapeXml(item.title)}</title>`,
|
|
32
|
+
` <link>${escapeXml(item.url)}</link>`,
|
|
33
|
+
` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
|
|
34
|
+
` <pubDate>${rfc822(item.date)}</pubDate>`,
|
|
35
|
+
` <description>${escapeXml(item.summary)}</description>`,
|
|
36
|
+
// CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
|
|
37
|
+
` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
|
|
38
|
+
' </item>',
|
|
39
|
+
].join('\n');
|
|
40
|
+
})
|
|
41
|
+
.join('\n');
|
|
42
|
+
return [
|
|
43
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
44
|
+
'<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
45
|
+
' <channel>',
|
|
46
|
+
` <title>${escapeXml(channel.title)}</title>`,
|
|
47
|
+
` <link>${escapeXml(channel.siteUrl)}</link>`,
|
|
48
|
+
` <description>${escapeXml(channel.description)}</description>`,
|
|
49
|
+
channel.language ? ` <language>${escapeXml(channel.language)}</language>` : '',
|
|
50
|
+
` <atom:link href="${escapeXml(channel.feedUrl)}" rel="self" type="application/rss+xml" />`,
|
|
51
|
+
entries,
|
|
52
|
+
' </channel>',
|
|
53
|
+
'</rss>',
|
|
54
|
+
'',
|
|
55
|
+
]
|
|
56
|
+
.filter((line) => line !== '')
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** Build a JSON Feed 1.1 document. */
|
|
60
|
+
export function buildJsonFeed(channel, items) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
version: 'https://jsonfeed.org/version/1.1',
|
|
63
|
+
title: channel.title,
|
|
64
|
+
description: channel.description,
|
|
65
|
+
home_page_url: channel.siteUrl,
|
|
66
|
+
feed_url: channel.feedUrl,
|
|
67
|
+
...(channel.language ? { language: channel.language } : {}),
|
|
68
|
+
...(channel.author ? { authors: [channel.author] } : {}),
|
|
69
|
+
items: items.map((item) => ({
|
|
70
|
+
id: item.url,
|
|
71
|
+
url: item.url,
|
|
72
|
+
title: item.title,
|
|
73
|
+
summary: item.summary,
|
|
74
|
+
date_published: iso(item.date),
|
|
75
|
+
...(item.updated ? { date_modified: iso(item.updated) } : {}),
|
|
76
|
+
...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
|
|
77
|
+
...(item.tags && item.tags.length ? { tags: item.tags } : {}),
|
|
78
|
+
})),
|
|
79
|
+
}, null, 2);
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A page of items plus its navigation state. */
|
|
2
|
+
export interface Page<T> {
|
|
3
|
+
items: T[];
|
|
4
|
+
page: number;
|
|
5
|
+
perPage: number;
|
|
6
|
+
total: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
hasPrev: boolean;
|
|
9
|
+
hasNext: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
|
|
12
|
+
export declare function paginate<T>(items: T[], page: number, perPage: number): Page<T>;
|
|
13
|
+
//# sourceMappingURL=paginate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paginate.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/paginate.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,MAAM,WAAW,IAAI,CAAC,CAAC;IACrB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAgB9E"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// cairn-cms: pagination helper (public-delivery design). Pure slice math; the template renders
|
|
2
|
+
// the controls. An out-of-range page clamps into bounds.
|
|
3
|
+
/** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
|
|
4
|
+
export function paginate(items, page, perPage) {
|
|
5
|
+
const total = items.length;
|
|
6
|
+
// A non-positive page size would make totalPages Infinity, so clamp it to one.
|
|
7
|
+
const size = Math.max(1, Math.floor(perPage) || 1);
|
|
8
|
+
const totalPages = Math.max(1, Math.ceil(total / size));
|
|
9
|
+
const current = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
|
|
10
|
+
const start = (current - 1) * size;
|
|
11
|
+
return {
|
|
12
|
+
items: items.slice(start, start + size),
|
|
13
|
+
page: current,
|
|
14
|
+
perPage: size,
|
|
15
|
+
total,
|
|
16
|
+
totalPages,
|
|
17
|
+
hasPrev: current > 1,
|
|
18
|
+
hasNext: current < totalPages,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/robots.ts"],"names":[],"mappings":"AAGA,+BAA+B;AAC/B,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,CAKrF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// cairn-cms: robots.txt builder (public-delivery design). A permissive default that points
|
|
2
|
+
// at the sitemap, with optional disallow rules.
|
|
3
|
+
/** Build a robots.txt body. */
|
|
4
|
+
export function buildRobots(opts) {
|
|
5
|
+
const lines = ['User-agent: *', 'Allow: /'];
|
|
6
|
+
for (const path of opts.disallow ?? [])
|
|
7
|
+
lines.push(`Disallow: ${path}`);
|
|
8
|
+
lines.push('', `Sitemap: ${opts.sitemapUrl}`, '');
|
|
9
|
+
return lines.join('\n');
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Inputs for the head. All URLs are absolute (built from PUBLIC_ORIGIN). */
|
|
2
|
+
export interface SeoInput {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
canonicalUrl: string;
|
|
6
|
+
siteName: string;
|
|
7
|
+
type?: 'website' | 'article';
|
|
8
|
+
published?: string;
|
|
9
|
+
modified?: string;
|
|
10
|
+
feeds?: {
|
|
11
|
+
rss?: string;
|
|
12
|
+
json?: string;
|
|
13
|
+
};
|
|
14
|
+
image?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Plain-data head: a title, meta tags, link tags, and one JSON-LD object. */
|
|
17
|
+
export interface SeoMeta {
|
|
18
|
+
title: string;
|
|
19
|
+
meta: {
|
|
20
|
+
name?: string;
|
|
21
|
+
property?: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}[];
|
|
24
|
+
links: {
|
|
25
|
+
rel: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
href: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
}[];
|
|
30
|
+
jsonLd: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/** Build the head data for a page. */
|
|
33
|
+
export declare function buildSeoMeta(input: SeoInput): SeoMeta;
|
|
34
|
+
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/seo.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,sCAAsC;AACtC,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CA6CrD"}
|