@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are recorded here, most recent first.
|
|
4
|
+
|
|
5
|
+
## 0.29.0
|
|
6
|
+
|
|
7
|
+
Consolidated the URL-identity model. A content entry's id, slug, date, and permalink are now derived in
|
|
8
|
+
one place (`entryIdentity`), so the content index and the manifest cannot drift on an entry's URL, and a
|
|
9
|
+
site's concept descriptors are resolved through one path shared by the admin runtime and the delivery
|
|
10
|
+
layer. No public surface changed.
|
|
11
|
+
|
|
12
|
+
The YAML URL policy is now validated at build. A permalink pattern must be root-relative and use only the
|
|
13
|
+
tokens `:slug`, `:year`, `:month`, and `:day`, a date token is valid only on a dated concept, a
|
|
14
|
+
`datePrefix` must be `year`, `month`, or `day`, and a policy keyed to an undeclared concept fails the
|
|
15
|
+
build.
|
|
16
|
+
|
|
17
|
+
Behavior note: a site whose `content:` URL policy was malformed and silently defaulted will now fail the
|
|
18
|
+
build with a named error. A valid policy is unaffected.
|
|
19
|
+
|
|
20
|
+
## 0.28.0
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
Closed the render attribute-sink residual by construction. A new post-dispatch guard runs last in
|
|
24
|
+
`createRenderer` and neutralizes the sinks a component `build()` could route a raw author attribute
|
|
25
|
+
value into, including the unsafe URL schemes `javascript:`, `data:`, and `vbscript:` in `href`,
|
|
26
|
+
`src`, `srcset`, `xlink:href`, `poster`, `formaction`, `action`, `object`'s `data`, and
|
|
27
|
+
`background`, the inline `on*` event handlers, and inline `style`, which is stripped wholesale. Safe
|
|
28
|
+
schemes, relative URLs, anchors, and the `cairn:` token are preserved. The guard is gated by the
|
|
29
|
+
existing `unsafeDisableSanitize` switch.
|
|
30
|
+
|
|
31
|
+
Behavior note: a site whose component `build()` emits a non-standard URL scheme, an `on*` handler,
|
|
32
|
+
or inline `style` will see that output neutralized. Route dynamic styling through a class or an
|
|
33
|
+
inert `data-*` attribute instead.
|
|
34
|
+
|
|
35
|
+
## 0.27.0
|
|
36
|
+
|
|
37
|
+
### Changed (breaking)
|
|
38
|
+
Narrowed the public export surface so each symbol has one canonical home. The `.` root and
|
|
39
|
+
`/sveltekit` no longer re-export another subpath's symbols, and the internal GitHub, signing, and
|
|
40
|
+
hast helpers left the public API. No symbol changed behavior; only where it exports from.
|
|
41
|
+
|
|
42
|
+
- Consumers must: import the delivery read helpers (`createContentIndex`, `createSiteIndexes`, the
|
|
43
|
+
feed, sitemap, robots, SEO, and pagination builders, `permalink`) from `@glw907/cairn-cms/delivery/data`
|
|
44
|
+
instead of the `.` root.
|
|
45
|
+
- Consumers must: import the public route loaders and the `*Response` helpers (`createPublicRoutes`,
|
|
46
|
+
`rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`) and the public route types
|
|
47
|
+
(`PublicRoutesDeps`, the public `ListData`, `TagData`, `TagIndexData`, `EntryData`) from
|
|
48
|
+
`@glw907/cairn-cms/delivery` instead of the `.` root or `/sveltekit`.
|
|
49
|
+
- Consumers must: stop importing the internal helpers that left the public API (`appJwt`,
|
|
50
|
+
`installationToken`, `signingSelfTest`, `appCredentials`, `treeUrl`, `contentsUrl`, `readRaw`,
|
|
51
|
+
`fileSha`, `listMarkdown`, `markdownFilesIn`, `commitFile`, `isElement`, `strProp`, `markFirstList`);
|
|
52
|
+
the engine wires GitHub token minting and the render pipeline internally, so no consumer needs them.
|
|
53
|
+
|
|
54
|
+
## 0.26.0
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- A `cairnManifest()` Vite plugin (`@glw907/cairn-cms/vite`) verifies the committed content manifest on
|
|
58
|
+
every build and fails the build with a diff naming what drifted. The check runs outside the prerender
|
|
59
|
+
lifecycle, so `handleHttpError` cannot mask it. Consumers must: add `cairnManifest({ configModule,
|
|
60
|
+
content, manifestPath })` to the Vite config.
|
|
61
|
+
- A `cairn-manifest` bin regenerates the committed manifest from a Vite context. Consumers must: set the
|
|
62
|
+
regenerate script to `"cairn:manifest": "cairn-manifest"` and delete the hand-written
|
|
63
|
+
`scripts/build-manifest.mjs`.
|
|
64
|
+
- A node-safe `@glw907/cairn-cms/delivery/data` entry exposes the pure delivery projections with no
|
|
65
|
+
`@sveltejs/kit` in the graph. Consumers must: move any plain-Node import of a delivery data helper
|
|
66
|
+
(such as `buildSiteManifest`) from `@glw907/cairn-cms/delivery` to `@glw907/cairn-cms/delivery/data`.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- `verifyManifest` now throws an error that names the added, removed, and changed entries. Consumers
|
|
70
|
+
must: nothing. The message is strictly more informative.
|
|
71
|
+
|
|
72
|
+
## 0.25.0
|
|
73
|
+
|
|
74
|
+
### Changed (breaking)
|
|
75
|
+
- `composeRuntime` now takes a single object, `composeRuntime({ adapter, siteConfig, extensions? })`,
|
|
76
|
+
and derives the per-concept URL policy from `siteConfig`. The loose third `urlPolicy` argument is
|
|
77
|
+
gone, and a missing `siteConfig` throws. Consumers must: pass the parsed site config to every
|
|
78
|
+
`composeRuntime` call and drop any hand-passed URL policy.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- `createRenderer()` now defaults its registry to the empty registry, so a plain-prose site calls
|
|
82
|
+
`createRenderer()` with no argument. Consumers must: nothing; passing a built registry is unchanged.
|
|
83
|
+
|
|
84
|
+
### Docs
|
|
85
|
+
- A render sanitize-floor reference (`docs/render-sanitize-floor.md`) states what the floor keeps,
|
|
86
|
+
strips, and rewrites, including the `target="_blank"` rel policy.
|
|
87
|
+
- An upgrade guide (`docs/upgrading.md`) collects the `0.x` renames with a consumer action each.
|
|
88
|
+
|
|
89
|
+
## 0.24.0
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
- `headRow(title, icon?)` builds the icon-plus-heading component head, exported beside `cardShell` and
|
|
93
|
+
`iconSpan`.
|
|
94
|
+
- A `createRenderer` `anchorRel` option sets the `rel` value forced on `target="_blank"` anchors
|
|
95
|
+
(default `'noopener noreferrer'`), or disables the injection when set to `false`.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
- A component's `defaultIconByRole` default now reaches the build through the declared `type: 'icon'`
|
|
99
|
+
attribute (`ctx.attributes`), so a role default no longer needs a hardcoded fallback in the build. A
|
|
100
|
+
component using `defaultIconByRole` must declare a `type: 'icon'` attribute.
|
|
101
|
+
- The engine drops an unclaimed directive `[label]` when a component has no `title` slot, so a stray
|
|
102
|
+
`[]` no longer renders an empty paragraph.
|
|
103
|
+
|
|
104
|
+
### Removed
|
|
105
|
+
- The internal `data-icon` marker, which no build read. The resolved icon now travels on the declared
|
|
106
|
+
attribute path.
|
|
107
|
+
|
|
108
|
+
## 0.23.0
|
|
109
|
+
|
|
110
|
+
### Changed (breaking)
|
|
111
|
+
- A `date` field now validates a real `YYYY-MM-DD` calendar date. A site adopting this version whose
|
|
112
|
+
committed content holds a malformed or impossible date will see it fail validation, which is the loud
|
|
113
|
+
failure this restores.
|
|
114
|
+
- A `tags` field now enforces its declared `options` as a closed vocabulary. A committed value outside
|
|
115
|
+
the list fails validation. Use a `freetags` field for free-form tags.
|
|
116
|
+
- `normalizeConcepts` now throws when a `summaryFields` key names no declared field, so a typo fails at
|
|
117
|
+
config load instead of silently producing an empty list card.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
- `AttributeField.options` is now `readonly string[]`, so a site can share one frozen `as const`
|
|
121
|
+
vocabulary across components. Read-only by use, so no call site changes.
|
|
122
|
+
|
|
123
|
+
## 0.22.0
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
- `ContentSummary.concept` and `EntryData.concept`: the read model carries its resolved concept id, so a
|
|
127
|
+
list or page branches per concept without re-deriving it from `entry.date`.
|
|
128
|
+
- A `summaryFields` knob on a concept config surfaces named frontmatter keys on `ContentSummary.fields`,
|
|
129
|
+
so a list card reads an authored field with no per-entry detail read.
|
|
130
|
+
- The package root re-exports the delivery route loaders (`createPublicRoutes`) and the response helpers
|
|
131
|
+
(`rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`).
|
|
132
|
+
|
|
133
|
+
### Changed (breaking)
|
|
134
|
+
- `CairnHead` moved off the `@glw907/cairn-cms/delivery` barrel to its own `@glw907/cairn-cms/delivery/head`
|
|
135
|
+
entry, so a node-environment data import from `/delivery` stays component-free. Update the import:
|
|
136
|
+
`import { CairnHead } from '@glw907/cairn-cms/delivery/head'`.
|
package/README.md
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
# cairn-cms
|
|
2
2
|
|
|
3
3
|
An embedded, **magic-link**, GitHub-committing CMS for SvelteKit + Cloudflare sites.
|
|
4
|
-
Non-technical authors log in by email (no GitHub account, no password), edit **raw
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
and auto-deploys.
|
|
4
|
+
Non-technical authors log in by email (no GitHub account, no password), edit **raw markdown**
|
|
5
|
+
in a client-only CodeMirror editor with a live preview, and save. Each save commits to `main`
|
|
6
|
+
via a **GitHub App** (committer = `cairn-cms[bot]`, author = the editor) and auto-deploys.
|
|
8
7
|
|
|
9
|
-
It is **design-agnostic
|
|
10
|
-
|
|
11
|
-
sites with completely different markdown pipelines
|
|
12
|
-
|
|
8
|
+
It is **design-agnostic**. Each consumer site supplies an adapter (the content contract through
|
|
9
|
+
`defineAdapter`/`defineFields`, the slug and permalink rules, and its render configuration), so the
|
|
10
|
+
same engine drives sites with completely different markdown pipelines. Two run in production today:
|
|
11
|
+
[ecnordic.ski](https://ecnordic.ski) (a remark-to-rehype directive pipeline) and
|
|
12
|
+
[907.life](https://907.life) (the engine's own `createRenderer`). Content is a fixed set of
|
|
13
|
+
first-class concepts (Posts and Pages), not open-ended collections.
|
|
13
14
|
|
|
14
15
|
## Status
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
cairn-cms runs two production sites today, [ecnordic.ski](https://ecnordic.ski) and
|
|
18
|
+
[907.life](https://907.life). It is `0.x` and breaks between minor versions. The author is
|
|
19
|
+
still working through the core-feature roadmap, and the project stays closely held until that
|
|
20
|
+
core lands. See the [ROADMAP](./ROADMAP.md) for what is planned and the
|
|
21
|
+
[CHANGELOG](./CHANGELOG.md) for what changed.
|
|
22
|
+
|
|
23
|
+
Editor auth is self-owned: an atomic single-use magic-link token, a POST-confirm flow, opaque
|
|
24
|
+
D1-backed session rows, and two-tier `owner`/`editor` roles. There is no better-auth, Drizzle,
|
|
25
|
+
or ORM. Pin a caret range and read the CHANGELOG before bumping; every breaking entry carries a
|
|
26
|
+
"Consumers must" line.
|
|
27
|
+
|
|
28
|
+
A contributor who feels inspired is welcome to open an issue or a discussion to start a
|
|
29
|
+
conversation. There is no formal contribution process yet, so this is not an open call for
|
|
30
|
+
pull requests.
|
|
30
31
|
|
|
31
32
|
## Install
|
|
32
33
|
|
|
@@ -34,23 +35,35 @@ range and expect 0.x churn.
|
|
|
34
35
|
npm install @glw907/cairn-cms
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`@glw907/cairn-cms/sveltekit` (server logic) and `@glw907/cairn-cms/components` (the admin UI).
|
|
38
|
+
Peer dependencies: `svelte@^5` and `@sveltejs/kit@^2`. A consumer site implements a `CairnAdapter`
|
|
39
|
+
and mounts thin `/admin` route shims around the package subpaths:
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
- `@glw907/cairn-cms`: the core engine and adapter contract.
|
|
42
|
+
- `@glw907/cairn-cms/sveltekit`: the server load and action logic.
|
|
43
|
+
- `@glw907/cairn-cms/components`: the admin Svelte UI.
|
|
44
|
+
- `@glw907/cairn-cms/delivery` and `/delivery/data`: the public read model (indexes, feeds,
|
|
45
|
+
sitemap, SEO head). The `/delivery/data` barrel is node-safe, with no `@sveltejs/kit` in its graph.
|
|
46
|
+
- `@glw907/cairn-cms/vite`: the `cairnManifest()` Vite plugin, paired with the `cairn-manifest` bin,
|
|
47
|
+
that builds and verifies the committed content manifest at build time.
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
Each site binds a Cloudflare D1 database as `AUTH_DB` (the editor allowlist, sessions, and single-use
|
|
50
|
+
magic tokens) and a `[[send_email]]` binding named `EMAIL`. The worked reference for every shape is
|
|
51
|
+
`examples/showcase`.
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
The [`docs/`](./docs/README.md) tree is organized in four arms: a tutorial that builds a first
|
|
56
|
+
site end to end, how-to guides for each setup task, a reference for every package export, and
|
|
57
|
+
explanation pages for the architecture and design rules. Start at the
|
|
58
|
+
[documentation index](./docs/README.md). The [security policy](./SECURITY.md) covers reporting
|
|
59
|
+
and the security posture.
|
|
60
|
+
|
|
61
|
+
## How it's developed
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
This is a standalone repo. Consumer sites install the published package from the npm registry by
|
|
64
|
+
version range. The library's own development proves changes against `examples/showcase`, a
|
|
65
|
+
self-contained SvelteKit site that consumes the package through the relative `file:../..` path, so a
|
|
66
|
+
change is exercised end to end before it publishes.
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
The historical rebuild plan and the early architecture writeups live under `docs/internal/`.
|
|
69
|
+
They are kept for history and are not current.
|
package/dist/auth/crypto.d.ts
CHANGED
|
@@ -16,4 +16,3 @@ export declare function generateToken(): string;
|
|
|
16
16
|
export declare function generateSessionId(): string;
|
|
17
17
|
/** The lowercase hex SHA-256 of a token, for storage and lookup. */
|
|
18
18
|
export declare function hashToken(token: string): Promise<string>;
|
|
19
|
-
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/auth/store.d.ts
CHANGED
|
@@ -40,4 +40,3 @@ export declare function setEditorRole(db: D1Database, email: string, role: Role)
|
|
|
40
40
|
* `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
|
|
41
41
|
*/
|
|
42
42
|
export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
|
|
43
|
-
//# sourceMappingURL=store.d.ts.map
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -17,4 +17,3 @@ interface Props {
|
|
|
17
17
|
declare const ComponentInsertDialog: import("svelte").Component<Props, {}, "">;
|
|
18
18
|
type ComponentInsertDialog = ReturnType<typeof ComponentInsertDialog>;
|
|
19
19
|
export default ComponentInsertDialog;
|
|
20
|
-
//# sourceMappingURL=ComponentInsertDialog.svelte.d.ts.map
|
|
@@ -12,4 +12,3 @@ export { default as NavTree } from './NavTree.svelte';
|
|
|
12
12
|
export { default as LinkPicker } from './LinkPicker.svelte';
|
|
13
13
|
export { default as DeleteDialog } from './DeleteDialog.svelte';
|
|
14
14
|
export { default as RenameDialog } from './RenameDialog.svelte';
|
|
15
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,4 +13,3 @@ export declare function linkCompletions(targets: LinkTarget[], query: string): C
|
|
|
13
13
|
* whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
|
|
14
14
|
* filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
|
|
15
15
|
export declare function cairnLinkCompletionSource(targets: LinkTarget[]): CompletionSource;
|
|
16
|
-
//# sourceMappingURL=link-completion.d.ts.map
|
|
@@ -30,4 +30,3 @@ export declare function unwrapCairnLink(doc: string, href: string): string;
|
|
|
30
30
|
* last to first, replacing only the `](oldHref` run so the label and title stay exact.
|
|
31
31
|
*/
|
|
32
32
|
export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
|
|
33
|
-
//# sourceMappingURL=markdown-format.d.ts.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { CairnAdapter } from './types.js';
|
|
2
2
|
/** Declare a site's adapter while preserving each concept's concrete schema type for typed reads. */
|
|
3
3
|
export declare function defineAdapter<const A extends CairnAdapter>(adapter: A): A;
|
|
4
|
-
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import type { CairnAdapter, CairnExtension, CairnRuntime
|
|
1
|
+
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
|
+
import type { SiteConfig } from '../nav/site-config.js';
|
|
3
|
+
/** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
4
|
+
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
5
|
+
* adapter's concepts. */
|
|
6
|
+
export interface ComposeInput {
|
|
7
|
+
adapter: CairnAdapter;
|
|
8
|
+
siteConfig: SiteConfig;
|
|
9
|
+
extensions?: CairnExtension[];
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
4
|
-
*
|
|
12
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
|
|
13
|
+
* is derived from the site config, the same source the delivery path uses, so the runtime and
|
|
14
|
+
* delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
|
|
15
|
+
* (seam 4) passes through untouched.
|
|
5
16
|
*/
|
|
6
|
-
export declare function composeRuntime(adapter
|
|
7
|
-
//# sourceMappingURL=compose.d.ts.map
|
|
17
|
+
export declare function composeRuntime({ adapter, siteConfig, extensions }: ComposeInput): CairnRuntime;
|
package/dist/content/compose.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveConcepts } from './concepts.js';
|
|
2
2
|
/**
|
|
3
|
-
* Fold an adapter and any extensions into the composed runtime (seam 2).
|
|
4
|
-
*
|
|
3
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
|
|
4
|
+
* is derived from the site config, the same source the delivery path uses, so the runtime and
|
|
5
|
+
* delivery permalinks cannot diverge. Extension concepts merge after the adapter's. The asset slot
|
|
6
|
+
* (seam 4) passes through untouched.
|
|
5
7
|
*/
|
|
6
|
-
export function composeRuntime(adapter, extensions = []
|
|
8
|
+
export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
|
|
9
|
+
if (!siteConfig)
|
|
10
|
+
throw new Error('composeRuntime needs a site config to derive the URL policy');
|
|
7
11
|
const content = { ...adapter.content };
|
|
8
12
|
const adminPanels = [];
|
|
9
13
|
const fieldTypes = [];
|
|
@@ -19,7 +23,7 @@ export function composeRuntime(adapter, extensions = [], urlPolicy = {}) {
|
|
|
19
23
|
}
|
|
20
24
|
return {
|
|
21
25
|
siteName: adapter.siteName,
|
|
22
|
-
concepts:
|
|
26
|
+
concepts: resolveConcepts(content, siteConfig),
|
|
23
27
|
backend: adapter.backend,
|
|
24
28
|
sender: adapter.sender,
|
|
25
29
|
render: adapter.render,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
2
|
+
import { type SiteConfig } from '../nav/site-config.js';
|
|
2
3
|
/**
|
|
3
4
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
4
5
|
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
@@ -13,6 +14,11 @@ export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
|
|
|
13
14
|
* a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
|
|
14
15
|
*/
|
|
15
16
|
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
19
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
20
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
|
|
16
23
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
17
24
|
export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
|
|
18
|
-
//# sourceMappingURL=concepts.d.ts.map
|
package/dist/content/concepts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
1
2
|
/**
|
|
2
3
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
3
4
|
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
@@ -17,6 +18,37 @@ function defaultLabel(id) {
|
|
|
17
18
|
function defaultPermalink(id) {
|
|
18
19
|
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
19
20
|
}
|
|
21
|
+
/** Permalink tokens the resolver understands. */
|
|
22
|
+
const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
|
|
23
|
+
/** The date-bearing tokens; valid only for a dated concept. */
|
|
24
|
+
const DATE_TOKENS = new Set(['year', 'month', 'day']);
|
|
25
|
+
/** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
|
|
26
|
+
const DATE_PREFIXES = new Set(['year', 'month', 'day']);
|
|
27
|
+
/**
|
|
28
|
+
* Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
|
|
29
|
+
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
30
|
+
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
31
|
+
*/
|
|
32
|
+
function validateUrlPolicy(id, policy, dated) {
|
|
33
|
+
if (policy.permalink !== undefined) {
|
|
34
|
+
const pattern = policy.permalink;
|
|
35
|
+
if (!pattern.startsWith('/')) {
|
|
36
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
|
|
37
|
+
}
|
|
38
|
+
for (const match of pattern.matchAll(/:(\w+)/g)) {
|
|
39
|
+
const token = match[1];
|
|
40
|
+
if (!KNOWN_TOKENS.has(token)) {
|
|
41
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
|
|
42
|
+
}
|
|
43
|
+
if (DATE_TOKENS.has(token) && !dated) {
|
|
44
|
+
throw new Error(`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
|
|
49
|
+
throw new Error(`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
20
52
|
/**
|
|
21
53
|
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
22
54
|
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
@@ -26,6 +58,12 @@ function defaultPermalink(id) {
|
|
|
26
58
|
*/
|
|
27
59
|
export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
|
|
28
60
|
const descriptors = [];
|
|
61
|
+
const declaredConcepts = new Set(Object.keys(content).filter((key) => content[key] !== undefined));
|
|
62
|
+
for (const key of Object.keys(urlPolicy)) {
|
|
63
|
+
if (!declaredConcepts.has(key)) {
|
|
64
|
+
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
for (const [id, config] of Object.entries(content)) {
|
|
30
68
|
if (!config)
|
|
31
69
|
continue;
|
|
@@ -35,12 +73,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
|
|
|
35
73
|
if (undeclared !== undefined) {
|
|
36
74
|
throw new Error(`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`);
|
|
37
75
|
}
|
|
76
|
+
const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
|
|
38
77
|
const policy = urlPolicy[id] ?? {};
|
|
78
|
+
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
39
79
|
descriptors.push({
|
|
40
80
|
id,
|
|
41
81
|
label: config.label ?? defaultLabel(id),
|
|
42
82
|
dir: config.dir,
|
|
43
|
-
routing:
|
|
83
|
+
routing: conceptRouting,
|
|
44
84
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
45
85
|
datePrefix: policy.datePrefix ?? 'day',
|
|
46
86
|
fields: config.schema.fields,
|
|
@@ -50,6 +90,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
|
|
|
50
90
|
}
|
|
51
91
|
return descriptors;
|
|
52
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
95
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
96
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveConcepts(content, siteConfig) {
|
|
99
|
+
return normalizeConcepts(content, urlPolicyFrom(siteConfig));
|
|
100
|
+
}
|
|
53
101
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
54
102
|
export function findConcept(concepts, id) {
|
|
55
103
|
return concepts.find((concept) => concept.id === id);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from './types.js';
|
|
2
|
+
/** A content entry's resolved URL identity. */
|
|
3
|
+
export interface EntryIdentity {
|
|
4
|
+
id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
date?: string;
|
|
7
|
+
permalink: string;
|
|
8
|
+
}
|
|
9
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
10
|
+
export declare function asString(value: unknown): string | undefined;
|
|
11
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
12
|
+
export declare function asDate(value: unknown): string | undefined;
|
|
13
|
+
/** Tags as an array, empty when the file declares none. */
|
|
14
|
+
export declare function asTags(value: unknown): string[];
|
|
15
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
16
|
+
export declare function entryId(path: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
19
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
20
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
21
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
22
|
+
*/
|
|
23
|
+
export declare function entryIdentity(descriptor: ConceptDescriptor, path: string, frontmatter: Record<string, unknown>): EntryIdentity;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
/** The basename of a glob path: the segment after the last slash, or the whole path. */
|
|
8
|
+
function basename(path) {
|
|
9
|
+
const slash = path.lastIndexOf('/');
|
|
10
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
11
|
+
}
|
|
12
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
13
|
+
export function asString(value) {
|
|
14
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
17
|
+
export function asDate(value) {
|
|
18
|
+
if (value instanceof Date)
|
|
19
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Tags as an array, empty when the file declares none. */
|
|
25
|
+
export function asTags(value) {
|
|
26
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
27
|
+
}
|
|
28
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
29
|
+
export function entryId(path) {
|
|
30
|
+
return idFromFilename(basename(path));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
34
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
35
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
36
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
37
|
+
*/
|
|
38
|
+
export function entryIdentity(descriptor, path, frontmatter) {
|
|
39
|
+
const id = entryId(path);
|
|
40
|
+
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
41
|
+
const date = asDate(frontmatter.date);
|
|
42
|
+
return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
|
|
43
|
+
}
|
package/dist/content/ids.d.ts
CHANGED
|
@@ -35,4 +35,3 @@ export declare function composeDatedId(date: string, slug: string, datePrefix: D
|
|
|
35
35
|
* newSlug. The caller validates newSlug with isValidId first.
|
|
36
36
|
*/
|
|
37
37
|
export declare function renameId(oldId: string, newSlug: string, datePrefix: DatePrefix | null): string;
|
|
38
|
-
//# sourceMappingURL=ids.d.ts.map
|