@glw907/cairn-cms 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/components/ConceptList.svelte +8 -4
  2. package/dist/components/ConceptList.svelte.d.ts.map +1 -1
  3. package/dist/components/EditPage.svelte +5 -5
  4. package/dist/components/EditPage.svelte.d.ts +3 -1
  5. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  6. package/dist/content/compose.d.ts +2 -2
  7. package/dist/content/compose.d.ts.map +1 -1
  8. package/dist/content/compose.js +3 -3
  9. package/dist/content/concepts.d.ts +7 -6
  10. package/dist/content/concepts.d.ts.map +1 -1
  11. package/dist/content/concepts.js +13 -5
  12. package/dist/content/ids.d.ts +14 -0
  13. package/dist/content/ids.d.ts.map +1 -1
  14. package/dist/content/ids.js +40 -0
  15. package/dist/content/permalink.d.ts +12 -0
  16. package/dist/content/permalink.d.ts.map +1 -0
  17. package/dist/content/permalink.js +30 -0
  18. package/dist/content/types.d.ts +23 -3
  19. package/dist/content/types.d.ts.map +1 -1
  20. package/dist/delivery/content-index.d.ts +47 -0
  21. package/dist/delivery/content-index.d.ts.map +1 -0
  22. package/dist/delivery/content-index.js +86 -0
  23. package/dist/delivery/excerpt.d.ts +11 -0
  24. package/dist/delivery/excerpt.d.ts.map +1 -0
  25. package/dist/delivery/excerpt.js +38 -0
  26. package/dist/delivery/feeds.d.ts +27 -0
  27. package/dist/delivery/feeds.d.ts.map +1 -0
  28. package/dist/delivery/feeds.js +80 -0
  29. package/dist/delivery/paginate.d.ts +13 -0
  30. package/dist/delivery/paginate.d.ts.map +1 -0
  31. package/dist/delivery/paginate.js +20 -0
  32. package/dist/delivery/robots.d.ts +6 -0
  33. package/dist/delivery/robots.d.ts.map +1 -0
  34. package/dist/delivery/robots.js +10 -0
  35. package/dist/delivery/seo.d.ts +34 -0
  36. package/dist/delivery/seo.d.ts.map +1 -0
  37. package/dist/delivery/seo.js +46 -0
  38. package/dist/delivery/site-index.d.ts +28 -0
  39. package/dist/delivery/site-index.d.ts.map +1 -0
  40. package/dist/delivery/site-index.js +38 -0
  41. package/dist/delivery/sitemap.d.ts +8 -0
  42. package/dist/delivery/sitemap.d.ts.map +1 -0
  43. package/dist/delivery/sitemap.js +21 -0
  44. package/dist/index.d.ts +19 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +14 -2
  47. package/dist/nav/site-config.d.ts +5 -0
  48. package/dist/nav/site-config.d.ts.map +1 -1
  49. package/dist/nav/site-config.js +4 -0
  50. package/dist/sveltekit/content-routes.d.ts.map +1 -1
  51. package/dist/sveltekit/content-routes.js +18 -8
  52. package/dist/sveltekit/index.d.ts +2 -0
  53. package/dist/sveltekit/index.d.ts.map +1 -1
  54. package/dist/sveltekit/index.js +1 -0
  55. package/dist/sveltekit/public-routes.d.ts +50 -0
  56. package/dist/sveltekit/public-routes.d.ts.map +1 -0
  57. package/dist/sveltekit/public-routes.js +45 -0
  58. package/package.json +1 -1
  59. package/src/lib/components/ConceptList.svelte +8 -4
  60. package/src/lib/components/EditPage.svelte +5 -5
  61. package/src/lib/content/compose.ts +4 -3
  62. package/src/lib/content/concepts.ts +15 -5
  63. package/src/lib/content/ids.ts +44 -0
  64. package/src/lib/content/permalink.ts +40 -0
  65. package/src/lib/content/types.ts +21 -4
  66. package/src/lib/delivery/content-index.ts +130 -0
  67. package/src/lib/delivery/excerpt.ts +41 -0
  68. package/src/lib/delivery/feeds.ts +112 -0
  69. package/src/lib/delivery/paginate.ts +32 -0
  70. package/src/lib/delivery/robots.ts +10 -0
  71. package/src/lib/delivery/seo.ts +72 -0
  72. package/src/lib/delivery/site-index.ts +68 -0
  73. package/src/lib/delivery/sitemap.ts +29 -0
  74. package/src/lib/index.ts +35 -1
  75. package/src/lib/nav/site-config.ts +8 -0
  76. package/src/lib/sveltekit/content-routes.ts +17 -7
  77. package/src/lib/sveltekit/index.ts +8 -0
  78. package/src/lib/sveltekit/public-routes.ts +83 -0
@@ -17,9 +17,14 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
17
17
  let title = $state('');
18
18
  let slug = $state('');
19
19
  let slugEdited = $state(false);
20
+ // Default the date client-side so the SSR pass and hydration agree across UTC midnight.
21
+ let dateDefault = $state('');
22
+ $effect(() => {
23
+ dateDefault = new Date().toISOString().slice(0, 10);
24
+ });
20
25
 
21
26
  const derivedSlug = $derived(slugEdited ? slug : slugify(title));
22
- const slugPlaceholder = $derived(data.dated ? '2026-05-my-entry' : 'about-us');
27
+ const slugPlaceholder = $derived(data.dated ? 'my-entry' : 'about-us');
23
28
  </script>
24
29
 
25
30
  <header class="mb-4 flex items-center justify-between">
@@ -58,14 +63,13 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
58
63
  <h2 class="text-sm font-semibold">New entry</h2>
59
64
  <label class="flex flex-col gap-1">
60
65
  <span class="text-sm font-medium">Title</span>
61
- <input class="input" name="title" aria-label="Title" bind:value={title} required />
66
+ <input class="input" name="title" bind:value={title} required />
62
67
  </label>
63
68
  <label class="flex flex-col gap-1">
64
69
  <span class="text-sm font-medium">Slug</span>
65
70
  <input
66
71
  class="input"
67
72
  name="slug"
68
- aria-label="Slug"
69
73
  placeholder={slugPlaceholder}
70
74
  value={derivedSlug}
71
75
  oninput={(e) => { slugEdited = true; slug = e.currentTarget.value; }}
@@ -74,7 +78,7 @@ plus a new-entry form. The slug auto-derives from the title until the author edi
74
78
  {#if data.dated}
75
79
  <label class="flex flex-col gap-1">
76
80
  <span class="text-sm font-medium">Date</span>
77
- <input class="input" type="date" name="date" aria-label="Date" />
81
+ <input class="input" type="date" name="date" value={dateDefault} />
78
82
  </label>
79
83
  {/if}
80
84
  <button type="submit" class="btn btn-primary self-start">Create</button>
@@ -1 +1 @@
1
- {"version":3,"file":"ConceptList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConceptList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG7D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,QAAQ,CAAC;CAChB;AAsEH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ConceptList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConceptList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG7D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,QAAQ,CAAC;CAChB;AA2EH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -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
- renderPreview?: (md: string) => string | Promise<string>;
24
+ render?: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
25
25
  }
26
26
 
27
- let { data, registry, preview = [], renderPreview }: Props = $props();
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 renderPreview call resolves after a newer one has started, the stale result is discarded.
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 || !renderPreview) return;
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 renderPreview(md);
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
- renderPreview?: (md: string) => string | Promise<string>;
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,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1D;AAqJH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
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"}
@@ -1,7 +1,7 @@
1
- import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
1
+ import type { CairnAdapter, CairnExtension, CairnRuntime, ConceptUrlPolicy } from './types.js';
2
2
  /**
3
3
  * Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
4
4
  * merge after the adapter's. The asset slot (seam 4) passes through untouched.
5
5
  */
6
- export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[]): CairnRuntime;
6
+ export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[], urlPolicy?: Record<string, ConceptUrlPolicy | undefined>): CairnRuntime;
7
7
  //# sourceMappingURL=compose.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/lib/content/compose.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,YAAY,EAAE,cAAc,EAAE,YAAY,EAA+B,MAAM,YAAY,CAAC;AAGtH;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,UAAU,GAAE,cAAc,EAAO,GAChC,YAAY,CAuBd"}
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/lib/content/compose.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,YAAY,EAAE,cAAc,EAAE,YAAY,EAAiB,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAGxI;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,UAAU,GAAE,cAAc,EAAO,EACjC,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAM,GAC3D,YAAY,CAuBd"}
@@ -3,7 +3,7 @@ import { normalizeConcepts } from './concepts.js';
3
3
  * Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
4
4
  * merge after the adapter's. The asset slot (seam 4) passes through untouched.
5
5
  */
6
- export function composeRuntime(adapter, extensions = []) {
6
+ export function composeRuntime(adapter, extensions = [], urlPolicy = {}) {
7
7
  const content = { ...adapter.content };
8
8
  const adminPanels = [];
9
9
  const fieldTypes = [];
@@ -19,10 +19,10 @@ export function composeRuntime(adapter, extensions = []) {
19
19
  }
20
20
  return {
21
21
  siteName: adapter.siteName,
22
- concepts: normalizeConcepts(content),
22
+ concepts: normalizeConcepts(content, urlPolicy),
23
23
  backend: adapter.backend,
24
24
  sender: adapter.sender,
25
- renderPreview: adapter.renderPreview,
25
+ render: adapter.render,
26
26
  registry: adapter.registry,
27
27
  navMenu: adapter.navMenu,
28
28
  assets: adapter.assets,
@@ -1,4 +1,4 @@
1
- import type { ConceptConfig, ConceptDescriptor, RoutingRule } from './types.js';
1
+ import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
2
2
  /**
3
3
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
4
4
  * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
@@ -6,12 +6,13 @@ import type { ConceptConfig, ConceptDescriptor, RoutingRule } from './types.js';
6
6
  */
7
7
  export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
8
8
  /**
9
- * Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each declared
10
- * key under `content` becomes one descriptor; an undeclared (`undefined`) concept is
11
- * skipped. `routing` is injectable so a contract test can prove a new concept attaches
12
- * additively; production passes the default `CONCEPT_ROUTING`.
9
+ * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
10
+ * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
11
+ * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
12
+ * otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
13
+ * a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
13
14
  */
14
- export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
15
+ export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
15
16
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
16
17
  export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
17
18
  //# sourceMappingURL=concepts.d.ts.map
@@ -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;AAUF;;;;;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,CAcrB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,EAAE,EAAE,MAAM,GACT,iBAAiB,GAAG,SAAS,CAE/B"}
1
+ {"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElG;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAGjE,CAAC;AAeF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,EAClD,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAM,EAC5D,OAAO,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAmB,GAC/D,iBAAiB,EAAE,CAiBrB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,EAAE,EAAE,MAAM,GACT,iBAAiB,GAAG,SAAS,CAE/B"}
@@ -13,22 +13,30 @@ 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
- * Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each declared
18
- * key under `content` becomes one descriptor; an undeclared (`undefined`) concept is
19
- * skipped. `routing` is injectable so a contract test can prove a new concept attaches
20
- * additively; production passes the default `CONCEPT_ROUTING`.
21
+ * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
22
+ * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
23
+ * concept id; each value defaults when the YAML omits it (`/:slug` for Pages, `/<id>/:slug`
24
+ * otherwise; `datePrefix` defaults to `day`). `routing` is injectable so a contract test can prove
25
+ * a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
21
26
  */
22
- export function normalizeConcepts(content, routing = CONCEPT_ROUTING) {
27
+ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
23
28
  const descriptors = [];
24
29
  for (const [id, config] of Object.entries(content)) {
25
30
  if (!config)
26
31
  continue;
32
+ const policy = urlPolicy[id] ?? {};
27
33
  descriptors.push({
28
34
  id,
29
35
  label: config.label ?? defaultLabel(id),
30
36
  dir: config.dir,
31
37
  routing: routing[id] ?? DEFAULT_ROUTING,
38
+ permalink: policy.permalink ?? defaultPermalink(id),
39
+ datePrefix: policy.datePrefix ?? 'day',
32
40
  fields: config.fields,
33
41
  validate: config.validate,
34
42
  });
@@ -14,4 +14,18 @@ export declare function filenameFromId(id: string): string;
14
14
  * single hyphen; leading and trailing hyphens are trimmed.
15
15
  */
16
16
  export declare function slugify(title: string): string;
17
+ /** Filename date-prefix granularity for a dated concept: the leading `YYYY[-MM[-DD]]-` on the stem. */
18
+ export type DatePrefix = 'year' | 'month' | 'day';
19
+ /**
20
+ * The URL slug for an id. A dated concept passes its `datePrefix` and the leading date prefix is
21
+ * stripped when present; a non-dated concept passes `null` and the id is returned verbatim. Only
22
+ * the leading prefix is removed, so a year-like tail (a post titled "2024 Recap") stays in the slug.
23
+ */
24
+ export declare function slugFromId(id: string, datePrefix: DatePrefix | null): string;
25
+ /**
26
+ * Compose a dated entry's id from a `YYYY-MM-DD` date, a date-free slug, and the concept's
27
+ * granularity: the date truncated to the granularity, a hyphen, then the slug. Throws on a
28
+ * malformed date so a bad create fails before touching git.
29
+ */
30
+ export declare function composeDatedId(date: string, slug: string, datePrefix: DatePrefix): string;
17
31
  //# sourceMappingURL=ids.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/content/ids.ts"],"names":[],"mappings":"AAOA,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
1
+ {"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/content/ids.ts"],"names":[],"mappings":"AAOA,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,uGAAuG;AACvG,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AASlD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,GAAG,MAAM,CAG5E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAiBzF"}
@@ -31,3 +31,43 @@ export function slugify(title) {
31
31
  .replace(/[^a-z0-9]+/g, '-')
32
32
  .replace(/^-+|-+$/g, '');
33
33
  }
34
+ /** The leading date-prefix shape for each granularity. */
35
+ const DATE_PREFIX_RE = {
36
+ year: /^\d{4}-/,
37
+ month: /^\d{4}-\d{2}-/,
38
+ day: /^\d{4}-\d{2}-\d{2}-/,
39
+ };
40
+ /**
41
+ * The URL slug for an id. A dated concept passes its `datePrefix` and the leading date prefix is
42
+ * stripped when present; a non-dated concept passes `null` and the id is returned verbatim. Only
43
+ * the leading prefix is removed, so a year-like tail (a post titled "2024 Recap") stays in the slug.
44
+ */
45
+ export function slugFromId(id, datePrefix) {
46
+ if (!datePrefix)
47
+ return id;
48
+ return id.replace(DATE_PREFIX_RE[datePrefix], '');
49
+ }
50
+ /**
51
+ * Compose a dated entry's id from a `YYYY-MM-DD` date, a date-free slug, and the concept's
52
+ * granularity: the date truncated to the granularity, a hyphen, then the slug. Throws on a
53
+ * malformed date so a bad create fails before touching git.
54
+ */
55
+ export function composeDatedId(date, slug, datePrefix) {
56
+ const m = date.match(/^(\d{4})-(\d{2})-(\d{2})$/);
57
+ if (!m)
58
+ throw new Error(`composeDatedId: malformed date "${date}"`);
59
+ const [, year, month, day] = m;
60
+ let prefix;
61
+ switch (datePrefix) {
62
+ case 'year':
63
+ prefix = year;
64
+ break;
65
+ case 'month':
66
+ prefix = `${year}-${month}`;
67
+ break;
68
+ case 'day':
69
+ prefix = `${year}-${month}-${day}`;
70
+ break;
71
+ }
72
+ return `${prefix}-${slug}`;
73
+ }
@@ -0,0 +1,12 @@
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
+ slug: string;
10
+ date?: string;
11
+ }): string;
12
+ //# 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,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,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.slug;
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
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ComponentRegistry } from '../render/registry.js';
2
+ import type { DatePrefix } from './ids.js';
2
3
  /** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
3
4
  interface FieldBase {
4
5
  /** Frontmatter key and form input name. */
@@ -69,6 +70,16 @@ export interface ConceptConfig {
69
70
  /** Validate submitted frontmatter before any commit. */
70
71
  validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
71
72
  }
73
+ /**
74
+ * A concept's URL policy, set per concept in the YAML site-config (not the adapter). `permalink` is
75
+ * a `/`-prefixed pattern of literal segments and the tokens `:slug`, `:year`, `:month`, `:day`.
76
+ * `datePrefix` is the filename date-prefix granularity for a dated concept. Both default in
77
+ * `normalizeConcepts` when omitted.
78
+ */
79
+ export interface ConceptUrlPolicy {
80
+ permalink?: string;
81
+ datePrefix?: DatePrefix;
82
+ }
72
83
  /** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
73
84
  export interface BackendConfig {
74
85
  owner: string;
@@ -114,8 +125,10 @@ export interface CairnAdapter {
114
125
  };
115
126
  backend: BackendConfig;
116
127
  sender: SenderConfig;
117
- /** Design-accurate preview: the same render pipeline the site ships. */
118
- renderPreview(md: string): string | Promise<string>;
128
+ /** The site's one renderer: the editor preview and every public page call it (design decision 4). */
129
+ render(md: string, opts?: {
130
+ stagger?: boolean;
131
+ }): string | Promise<string>;
119
132
  /** Directive component registry; the renderer and the future palette derive from it (seam 3). */
120
133
  registry?: ComponentRegistry;
121
134
  navMenu?: NavMenuConfig;
@@ -143,6 +156,10 @@ export interface ConceptDescriptor {
143
156
  label: string;
144
157
  dir: string;
145
158
  routing: RoutingRule;
159
+ /** The resolved permalink pattern, defaulted by `normalizeConcepts`. */
160
+ permalink: string;
161
+ /** Filename date-prefix granularity for a dated concept; resolved by `normalizeConcepts`. */
162
+ datePrefix: DatePrefix;
146
163
  fields: FrontmatterField[];
147
164
  validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
148
165
  }
@@ -197,7 +214,10 @@ export interface CairnRuntime {
197
214
  concepts: ConceptDescriptor[];
198
215
  backend: BackendConfig;
199
216
  sender: SenderConfig;
200
- renderPreview(md: string): string | Promise<string>;
217
+ /** The site's one renderer: the editor preview and every public page call it (design decision 4). */
218
+ render(md: string, opts?: {
219
+ stagger?: boolean;
220
+ }): string | Promise<string>;
201
221
  registry?: ComponentRegistry;
202
222
  navMenu?: NavMenuConfig;
203
223
  assets?: AssetConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
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
+ slug: string;
11
+ permalink: string;
12
+ title: string;
13
+ date?: string;
14
+ updated?: string;
15
+ tags: string[];
16
+ excerpt: string;
17
+ wordCount: number;
18
+ draft: boolean;
19
+ }
20
+ /** The detail view: a summary plus the frontmatter and the body to render. */
21
+ export interface ContentEntry extends ContentSummary {
22
+ frontmatter: Record<string, unknown>;
23
+ body: string;
24
+ }
25
+ /** The per-concept query surface. */
26
+ export interface ContentIndex {
27
+ all(opts?: {
28
+ includeDrafts?: boolean;
29
+ }): ContentSummary[];
30
+ byId(id: string): ContentEntry | undefined;
31
+ byTag(tag: string, opts?: {
32
+ includeDrafts?: boolean;
33
+ }): ContentSummary[];
34
+ allTags(): {
35
+ tag: string;
36
+ count: number;
37
+ }[];
38
+ adjacent(id: string): {
39
+ newer?: ContentSummary;
40
+ older?: ContentSummary;
41
+ };
42
+ }
43
+ /** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
44
+ export declare function fromGlob(record: Record<string, string>): RawFile[];
45
+ /** Build a concept's index from its raw files and normalized descriptor. */
46
+ export declare function createContentIndex(files: RawFile[], descriptor: ConceptDescriptor): ContentIndex;
47
+ //# 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,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,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,CA2DhG"}
@@ -0,0 +1,86 @@
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, slugFromId } 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 slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
35
+ const { frontmatter, body } = parseMarkdown(file.raw);
36
+ const date = asDate(frontmatter.date);
37
+ return {
38
+ id,
39
+ slug,
40
+ permalink: permalink(descriptor, { id, slug, date }),
41
+ title: asString(frontmatter.title) ?? id,
42
+ date,
43
+ updated: asDate(frontmatter.updated),
44
+ tags: asTags(frontmatter.tags),
45
+ excerpt: deriveExcerpt(body, { description: asString(frontmatter.description) }),
46
+ wordCount: wordCount(body),
47
+ draft: frontmatter.draft === true,
48
+ frontmatter,
49
+ body,
50
+ };
51
+ });
52
+ // Dated concepts sort newest-first; undated concepts (Pages) sort by title.
53
+ const sorted = [...entries].sort((a, b) => descriptor.routing.dated ? (b.date ?? '').localeCompare(a.date ?? '') : a.title.localeCompare(b.title));
54
+ const summarize = (entry) => {
55
+ const { frontmatter: _frontmatter, body: _body, ...summary } = entry;
56
+ return summary;
57
+ };
58
+ const visible = (list, includeDrafts) => includeDrafts ? list : list.filter((entry) => !entry.draft);
59
+ return {
60
+ all: (opts = {}) => visible(sorted, opts.includeDrafts).map(summarize),
61
+ byId: (id) => entries.find((entry) => entry.id === id),
62
+ byTag: (tag, opts = {}) => visible(sorted, opts.includeDrafts)
63
+ .filter((entry) => entry.tags.includes(tag))
64
+ .map(summarize),
65
+ allTags: () => {
66
+ const counts = new Map();
67
+ for (const entry of sorted) {
68
+ if (entry.draft)
69
+ continue;
70
+ for (const tag of entry.tags)
71
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
72
+ }
73
+ return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => a.tag.localeCompare(b.tag));
74
+ },
75
+ adjacent: (id) => {
76
+ const list = visible(sorted, false);
77
+ const i = list.findIndex((entry) => entry.id === id);
78
+ if (i < 0)
79
+ return {};
80
+ return {
81
+ newer: i > 0 ? summarize(list[i - 1]) : undefined,
82
+ older: i < list.length - 1 ? summarize(list[i + 1]) : undefined,
83
+ };
84
+ },
85
+ };
86
+ }
@@ -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"}