caracms 0.0.0-stage → 0.2.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 (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/bin/caracms.mjs +174 -0
  4. package/bin/gate.mjs +275 -0
  5. package/bin/init.mjs +712 -0
  6. package/bin/media.mjs +662 -0
  7. package/bin/site-runner.mjs +61 -0
  8. package/bin/templates/Page.astro.txt +31 -0
  9. package/bin/templates/TextBlock.astro.txt +13 -0
  10. package/bin/templates/caracms.config.ts.txt +22 -0
  11. package/bin/templates/catch-all.astro.txt +25 -0
  12. package/bin/templates/health.ts.txt +6 -0
  13. package/bin/templates/railway.ts.txt +147 -0
  14. package/package.json +66 -4
  15. package/skills/caracms-adopt/SKILL.md +508 -0
  16. package/skills/caracms-adopt/look-same.mjs +957 -0
  17. package/skills/caracms-blocks/SKILL.md +376 -0
  18. package/skills/caracms-content/SKILL.md +201 -0
  19. package/skills/caracms-tina/SKILL.md +427 -0
  20. package/skills/caracms-tina/field-kinds.md +88 -0
  21. package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
  22. package/src/block-runs.ts +84 -0
  23. package/src/cards.ts +158 -0
  24. package/src/cli/schemas.ts +122 -0
  25. package/src/cli/site.ts +66 -0
  26. package/src/cli/validate.ts +471 -0
  27. package/src/code-routes-disk.ts +67 -0
  28. package/src/code-routes.ts +134 -0
  29. package/src/columns.ts +39 -0
  30. package/src/components/Block.astro +46 -0
  31. package/src/components/BlockRunTree.astro +45 -0
  32. package/src/components/Blocks.astro +139 -0
  33. package/src/components/Buttons.astro +116 -0
  34. package/src/components/Fonts.astro +14 -0
  35. package/src/components/Form.astro +221 -0
  36. package/src/components/Head.astro +62 -0
  37. package/src/components/Link.astro +68 -0
  38. package/src/components/Lockup.astro +116 -0
  39. package/src/components/Media.astro +124 -0
  40. package/src/components/MediaPlayer.astro +102 -0
  41. package/src/components/RenderDocument.astro +64 -0
  42. package/src/components/RichText.astro +59 -0
  43. package/src/components/Text.astro +81 -0
  44. package/src/components/attributes.ts +33 -0
  45. package/src/components/descriptor.ts +45 -0
  46. package/src/components/form-client.ts +182 -0
  47. package/src/components/media-player.ts +46 -0
  48. package/src/components/media-props.ts +35 -0
  49. package/src/components/sitemap.xml.ts +36 -0
  50. package/src/config.ts +825 -0
  51. package/src/content-schema.ts +166 -0
  52. package/src/content.ts +422 -0
  53. package/src/document.ts +579 -0
  54. package/src/editor/README.md +405 -0
  55. package/src/editor/addresses.ts +61 -0
  56. package/src/editor/brand.ts +118 -0
  57. package/src/editor/client/block-list/BlockList.tsx +342 -0
  58. package/src/editor/client/block-list/Picker.tsx +105 -0
  59. package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
  60. package/src/editor/client/block-list/insert-points.ts +56 -0
  61. package/src/editor/client/block-list/link.ts +71 -0
  62. package/src/editor/client/block-list/reorder.ts +105 -0
  63. package/src/editor/client/block-list/summary.ts +118 -0
  64. package/src/editor/client/collections/CollectionGrid.tsx +143 -0
  65. package/src/editor/client/collections/CollectionTable.tsx +406 -0
  66. package/src/editor/client/collections/table.ts +270 -0
  67. package/src/editor/client/context.tsx +27 -0
  68. package/src/editor/client/documents/DocMenu.tsx +154 -0
  69. package/src/editor/client/documents/actions.ts +192 -0
  70. package/src/editor/client/documents/dialogs.tsx +687 -0
  71. package/src/editor/client/editor.ts +995 -0
  72. package/src/editor/client/fields/ButtonsField.tsx +87 -0
  73. package/src/editor/client/fields/DateField.tsx +200 -0
  74. package/src/editor/client/fields/FieldControl.tsx +61 -0
  75. package/src/editor/client/fields/FieldFrame.tsx +109 -0
  76. package/src/editor/client/fields/GroupField.tsx +113 -0
  77. package/src/editor/client/fields/HiddenToggle.tsx +51 -0
  78. package/src/editor/client/fields/LinkField.tsx +307 -0
  79. package/src/editor/client/fields/ListField.tsx +642 -0
  80. package/src/editor/client/fields/LockupField.tsx +365 -0
  81. package/src/editor/client/fields/MediaField.tsx +823 -0
  82. package/src/editor/client/fields/PageFields.tsx +82 -0
  83. package/src/editor/client/fields/ReferenceField.tsx +209 -0
  84. package/src/editor/client/fields/RichTextField.tsx +306 -0
  85. package/src/editor/client/fields/SelectField.tsx +63 -0
  86. package/src/editor/client/fields/SeoGroup.tsx +282 -0
  87. package/src/editor/client/fields/SimpleFields.tsx +89 -0
  88. package/src/editor/client/fields/TextField.tsx +51 -0
  89. package/src/editor/client/fields/date.ts +73 -0
  90. package/src/editor/client/fields/host.tsx +172 -0
  91. package/src/editor/client/fields/list-summary.ts +85 -0
  92. package/src/editor/client/fields/lockup-rules.ts +181 -0
  93. package/src/editor/client/fields/parts.tsx +458 -0
  94. package/src/editor/client/fields/rich-text-commands.ts +183 -0
  95. package/src/editor/client/fields/rich-text-doc.ts +156 -0
  96. package/src/editor/client/fields/rich-text-schema.ts +224 -0
  97. package/src/editor/client/fields/stale.ts +58 -0
  98. package/src/editor/client/inbox/InboxStage.tsx +272 -0
  99. package/src/editor/client/inbox/store.ts +311 -0
  100. package/src/editor/client/live/format.ts +77 -0
  101. package/src/editor/client/live/marks.tsx +90 -0
  102. package/src/editor/client/live/notices.tsx +213 -0
  103. package/src/editor/client/live/publish.tsx +617 -0
  104. package/src/editor/client/live/session.tsx +162 -0
  105. package/src/editor/client/main.tsx +12 -0
  106. package/src/editor/client/media/LibraryStage.tsx +75 -0
  107. package/src/editor/client/media/ReplaceDialog.tsx +355 -0
  108. package/src/editor/client/media/store.ts +618 -0
  109. package/src/editor/client/nav/DocRow.tsx +122 -0
  110. package/src/editor/client/nav/search.ts +71 -0
  111. package/src/editor/client/nav/tree.ts +93 -0
  112. package/src/editor/client/overlays/OverlayMount.tsx +26 -0
  113. package/src/editor/client/overlays/editor-marks.ts +159 -0
  114. package/src/editor/client/overlays/index.ts +50 -0
  115. package/src/editor/client/overlays/layer.ts +437 -0
  116. package/src/editor/client/overlays/overlay-css.ts +177 -0
  117. package/src/editor/client/overlays/rich-text-bar.ts +717 -0
  118. package/src/editor/client/pane/Pane.tsx +91 -0
  119. package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
  120. package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
  121. package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
  122. package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
  123. package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
  124. package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
  125. package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
  126. package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
  127. package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
  128. package/src/editor/client/pane/parts.tsx +179 -0
  129. package/src/editor/client/pane/switcher.tsx +165 -0
  130. package/src/editor/client/preview/driver.ts +640 -0
  131. package/src/editor/client/preview/field-text.ts +71 -0
  132. package/src/editor/client/preview/follow-links.ts +234 -0
  133. package/src/editor/client/preview/layout.ts +37 -0
  134. package/src/editor/client/preview/link-target.ts +50 -0
  135. package/src/editor/client/preview/markers.ts +86 -0
  136. package/src/editor/client/preview/page-edit.ts +632 -0
  137. package/src/editor/client/preview/page-editor-registry.ts +63 -0
  138. package/src/editor/client/preview/page-editor.ts +235 -0
  139. package/src/editor/client/preview/render-sequencer.ts +113 -0
  140. package/src/editor/client/preview/selection.ts +116 -0
  141. package/src/editor/client/preview/warnings.ts +61 -0
  142. package/src/editor/client/shell/App.tsx +101 -0
  143. package/src/editor/client/shell/Bar.tsx +197 -0
  144. package/src/editor/client/shell/Brand.tsx +33 -0
  145. package/src/editor/client/shell/Icons.tsx +87 -0
  146. package/src/editor/client/shell/NarrowNote.tsx +47 -0
  147. package/src/editor/client/shell/Popovers.tsx +47 -0
  148. package/src/editor/client/shell/Stage.tsx +132 -0
  149. package/src/editor/client/shell/Toast.tsx +61 -0
  150. package/src/editor/client/state/document-store.ts +465 -0
  151. package/src/editor/client/state/keeper.ts +713 -0
  152. package/src/editor/client/state/live.ts +257 -0
  153. package/src/editor/client/state/schema.ts +42 -0
  154. package/src/editor/client/state/store.ts +39 -0
  155. package/src/editor/fonts/OFL-GeistMono.txt +93 -0
  156. package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
  157. package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  158. package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
  159. package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  160. package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
  161. package/src/editor/middleware.ts +62 -0
  162. package/src/editor/routes/_live.ts +26 -0
  163. package/src/editor/routes/admin.astro +80 -0
  164. package/src/editor/routes/create.ts +19 -0
  165. package/src/editor/routes/delete.ts +19 -0
  166. package/src/editor/routes/document.ts +39 -0
  167. package/src/editor/routes/documents.ts +30 -0
  168. package/src/editor/routes/github.ts +23 -0
  169. package/src/editor/routes/history.ts +9 -0
  170. package/src/editor/routes/lock.ts +9 -0
  171. package/src/editor/routes/login/assets.d.ts +10 -0
  172. package/src/editor/routes/login/login.ts +43 -0
  173. package/src/editor/routes/media.ts +16 -0
  174. package/src/editor/routes/move.ts +17 -0
  175. package/src/editor/routes/notice.ts +9 -0
  176. package/src/editor/routes/preview.astro +77 -0
  177. package/src/editor/routes/publish.ts +9 -0
  178. package/src/editor/routes/status.ts +9 -0
  179. package/src/editor/routes/submissions.ts +16 -0
  180. package/src/editor/routes/try-again.ts +9 -0
  181. package/src/editor/routes/write.ts +18 -0
  182. package/src/editor/server/admin-props.ts +151 -0
  183. package/src/editor/server/cards.ts +61 -0
  184. package/src/editor/server/create.ts +414 -0
  185. package/src/editor/server/drafts-store.ts +127 -0
  186. package/src/editor/server/media/alt.ts +107 -0
  187. package/src/editor/server/media/bucket.ts +295 -0
  188. package/src/editor/server/media/config.ts +78 -0
  189. package/src/editor/server/media/handler.ts +221 -0
  190. package/src/editor/server/media/library.ts +254 -0
  191. package/src/editor/server/media/memory.ts +184 -0
  192. package/src/editor/server/media/mux.ts +241 -0
  193. package/src/editor/server/media/sniff.ts +599 -0
  194. package/src/editor/server/media/upload.ts +175 -0
  195. package/src/editor/server/mode.ts +63 -0
  196. package/src/editor/server/move.ts +295 -0
  197. package/src/editor/server/render.tsx +9 -0
  198. package/src/editor/server/replace.ts +72 -0
  199. package/src/editor/server/settings.ts +35 -0
  200. package/src/editor/server/write.ts +366 -0
  201. package/src/editor/styles/editor.css +502 -0
  202. package/src/editor/styles/fonts.css +40 -0
  203. package/src/editor/styles/levels/README.md +4 -0
  204. package/src/editor/styles/levels/collections.css +99 -0
  205. package/src/editor/styles/levels/document-level.css +10 -0
  206. package/src/editor/styles/levels/documents.css +9 -0
  207. package/src/editor/styles/levels/fields.css +171 -0
  208. package/src/editor/styles/levels/files.css +95 -0
  209. package/src/editor/styles/levels/live.css +10 -0
  210. package/src/editor/styles/levels/m5a-documents.css +51 -0
  211. package/src/editor/styles/levels/m6.css +82 -0
  212. package/src/editor/styles/levels/media.css +175 -0
  213. package/src/editor/styles/levels/switcher.css +3 -0
  214. package/src/editor/styles/tokens.css +114 -0
  215. package/src/editor/tsconfig.json +16 -0
  216. package/src/editor/types.ts +881 -0
  217. package/src/editor/ui-state.ts +113 -0
  218. package/src/email.ts +67 -0
  219. package/src/fields.ts +1129 -0
  220. package/src/files-route.ts +201 -0
  221. package/src/files.ts +116 -0
  222. package/src/forms/email.ts +149 -0
  223. package/src/forms/endpoint.ts +347 -0
  224. package/src/forms/inbox.ts +273 -0
  225. package/src/forms/rate-limit.ts +130 -0
  226. package/src/forms/route.ts +24 -0
  227. package/src/forms/rules.ts +278 -0
  228. package/src/forms/store.ts +486 -0
  229. package/src/head.ts +125 -0
  230. package/src/index.ts +434 -0
  231. package/src/json-source.ts +116 -0
  232. package/src/links.ts +121 -0
  233. package/src/live/README.md +43 -0
  234. package/src/live/auth/allow-rules.ts +70 -0
  235. package/src/live/auth/allow.ts +128 -0
  236. package/src/live/auth/auth.ts +155 -0
  237. package/src/live/auth/email.ts +84 -0
  238. package/src/live/auth/guard.ts +111 -0
  239. package/src/live/auth/index.ts +68 -0
  240. package/src/live/auth/links.ts +101 -0
  241. package/src/live/auth/login.ts +245 -0
  242. package/src/live/auth/middleware.ts +53 -0
  243. package/src/live/auth/request.ts +56 -0
  244. package/src/live/auth/routes.ts +9 -0
  245. package/src/live/auth/session.ts +181 -0
  246. package/src/live/auth/views.ts +355 -0
  247. package/src/live/autosaves.ts +306 -0
  248. package/src/live/backend.ts +652 -0
  249. package/src/live/branches.ts +19 -0
  250. package/src/live/drafts-store.ts +591 -0
  251. package/src/live/editor.ts +465 -0
  252. package/src/live/git.ts +1021 -0
  253. package/src/live/github.ts +271 -0
  254. package/src/live/locals.ts +24 -0
  255. package/src/live/locks.ts +273 -0
  256. package/src/live/merge.ts +197 -0
  257. package/src/live/messages.ts +151 -0
  258. package/src/live/mode.ts +210 -0
  259. package/src/live/redis.ts +533 -0
  260. package/src/live/runtime.ts +164 -0
  261. package/src/live/status.ts +109 -0
  262. package/src/live/sweep.ts +228 -0
  263. package/src/live/timings.ts +47 -0
  264. package/src/live/webhook.ts +39 -0
  265. package/src/lockup-css.ts +58 -0
  266. package/src/measure.ts +145 -0
  267. package/src/media-value.ts +239 -0
  268. package/src/media.ts +255 -0
  269. package/src/read.ts +256 -0
  270. package/src/redirect-middleware.ts +49 -0
  271. package/src/redirects.ts +76 -0
  272. package/src/relink.ts +293 -0
  273. package/src/reserved.ts +24 -0
  274. package/src/rich-text.ts +690 -0
  275. package/src/schema-key.ts +63 -0
  276. package/src/seo.ts +124 -0
  277. package/src/serialize.ts +697 -0
  278. package/src/skipped.ts +35 -0
  279. package/src/slug.ts +121 -0
  280. package/src/text-styles.ts +62 -0
  281. package/src/virtual.d.ts +28 -0
@@ -0,0 +1,508 @@
1
+ ---
2
+ name: caracms-adopt
3
+ description: Bring an existing Astro site onto caracms — run `caracms init`, then convert the site's pages to caracms documents one at a time (its layout into the page component, its sections into blocks, its content collections into caracms collections, its settings into Site Settings), move its images with `caracms media import`, and prove visitors' pages look the same with the look-same script. Use when adding caracms to a site, or converting a page, collection or the header and footer of one.
4
+ ---
5
+
6
+ # Bringing an existing Astro site onto caracms
7
+
8
+ **The bar: visitors' pages look the same.** Moving to caracms changes what editors can do,
9
+ not what visitors see. Every converted page is checked against the site as it was, with
10
+ the look-same script below.
11
+
12
+ Read `caracms-blocks` for how blocks, collections and the page component work, and
13
+ `caracms-content` for the content files. This skill is the order to do things in.
14
+
15
+ Commands run from the site's root: `npx caracms …`, or `pnpm caracms …` in a pnpm site.
16
+
17
+ ## 1. Add the plumbing: `caracms init`
18
+
19
+ Start from a clean git checkout (untracked files are fine). The order is:
20
+
21
+ 1. **Install caracms**, pinned, if `package.json` has none yet: `npm install caracms --save-exact`
22
+ (or `pnpm add caracms --save-exact`).
23
+ 2. **Commit the install** (`package.json` and the lockfile). `init --write` refuses while
24
+ tracked files have uncommitted changes, so that its own change is one diff to review.
25
+ 3. **`npx caracms init`**, the dry run (below), and read it.
26
+ 4. **`npx caracms init --write`**.
27
+ 5. **Install again** (`npm install` / `pnpm install`): `init` adds dependencies to
28
+ `package.json`.
29
+
30
+ **`npx caracms init`** is a dry run: it writes nothing and prints
31
+
32
+ - one line per file: `add` (a new file), `edit` (an existing one), `link` / `unlink` (a
33
+ skill link in `.claude/skills/`);
34
+ - a diff of each;
35
+ - `Already done: …` on a site partly set up;
36
+ - `Notes:`, things it can't do itself, with the lines to add by hand (an `astro.config` it
37
+ can't parse, a `tsconfig.json` it can't follow, a `.railway/railway.ts` the site already
38
+ has, an existing `/health` route);
39
+ - what to run next.
40
+
41
+ If something is in the way it prints `caracms init: N things are in the way, so nothing
42
+ was planned:` with each problem and what to do, and exits 1. Fix them all and run it
43
+ again. The blockers: a `content/` folder holding files that aren't caracms's; a route or
44
+ `public/` folder at `/admin`, `/_caracms` or `/files`; the site's own `/sitemap.xml` or
45
+ `/robots.txt` route (caracms serves both); a route that already catches every top-level
46
+ address (`src/pages/[...slug].astro`); an adapter other than `@astrojs/node`, or the Node
47
+ adapter in middleware mode; an Astro outside caracms's range, or none; a Tina site
48
+ (`tinacms` in its dependencies, or tracked files under `tina/`: those come in through a
49
+ port, not `init`; an untracked leftover `tina/__generated__/` is only a note). `init --write` also refuses with uncommitted changes
50
+ to tracked files: usually the install of step 1, not yet committed.
51
+
52
+ Read the diff. Then **`npx caracms init --write`**, install again (`npm install` /
53
+ `pnpm install`), and commit that as one change.
54
+
55
+ pnpm 12.11 and later may print `Agent skills awaiting approval: caracms` after the install.
56
+ That's pnpm's own gate: it links the skills a dependency ships into `.claude/skills/` only
57
+ once you approve them with `pnpm approve` (`pnpm permissions` lists what's waiting).
58
+ `init` has already linked caracms's skills itself, so there's nothing to do; leave it
59
+ unapproved, and don't approve it in passing (`pnpm approve` also reviews build scripts).
60
+
61
+ `init` added:
62
+
63
+ - `caracms.config.ts`: a `pages` collection at `/` (nested), one `text` block, no text
64
+ styles;
65
+ - `src/components/caracms/Page.astro` (the page component) and `TextBlock.astro`;
66
+ - `src/pages/[...path].astro`, which serves the documents in `content/pages/` and nothing
67
+ else, and `src/pages/health.ts`;
68
+ - `content/site-settings.json` (its `brand.name` is the package name title-cased: correct
69
+ it), and `schemas/`;
70
+ - `astro.config` edits: the Node adapter (standalone), `integrations: [caracms(site)]`,
71
+ `devToolbar: { enabled: false }`;
72
+ - `package.json`: `@astrojs/node`, `railway` (dev), `"start": "caracms start"`;
73
+ `.gitignore`: `.caracms/`; the tsconfig's `allowImportingTsExtensions`;
74
+ - `.railway/railway.ts` (when the site had none), the skill links and a block in
75
+ `CLAUDE.md`.
76
+
77
+ Then `astro build`: every existing page must still build. The pages stay prerendered, but
78
+ the output moves to `dist/client/` and `dist/server/`, and `npm start` is now
79
+ `caracms start` (`PORT`, default 8080). Anything in the site that reads `dist/` directly
80
+ (its own built-site checks, a script) needs the new paths: the pages are in
81
+ `dist/client/`.
82
+
83
+ Re-run `init` after updating caracms: it re-links the skills and refreshes the CLAUDE.md
84
+ block and the schemas. On a set-up site with nothing to do it says
85
+ `caracms init: nothing to do. caracms is set up here.`
86
+
87
+ ## 2. Set up the comparison before changing a page
88
+
89
+ Keep the site as it was running, to compare against: a git worktree at the commit before
90
+ `init`, built and served the way the site served it.
91
+
92
+ ```sh
93
+ git worktree add ../<site>-baseline <commit before init>
94
+ ```
95
+
96
+ Install and build there and serve it on its own port. The new site is `astro dev`
97
+ (give it a port of its own: `astro dev --port <port>`), or a build served with
98
+ `PORT=<port> npm start`. For look-same's HTML diff both must be builds (section 9).
99
+
100
+ **Run the site's own tests now**, before any page moves, and note what passes. Tests that
101
+ read the old routes (`src/pages/about.astro`), the old collection files (YAML, Markdown)
102
+ or `dist/` will break as pages move. When they do, change where they read (the documents
103
+ are `content/pages/<path>.json` and `content/<collection>/<slug>.json`, the built pages
104
+ `dist/client/`), not what they check. A check that a page's own CSS isn't on another page
105
+ is the exception (section 4).
106
+
107
+ **Images need the media variables** in the site's `.env` (never committed):
108
+ `CARACMS_MEDIA_URL` (imgproxy's origin), `IMGPROXY_KEY`, `IMGPROXY_SALT` and `S3_BUCKET`
109
+ to show images, plus `S3_ENDPOINT`, `S3_REGION`, `S3_ACCESS_KEY_ID` and
110
+ `S3_SECRET_ACCESS_KEY` to import and upload them. Without them `astro dev` draws a
111
+ caracms image as nothing, and `astro build` stops with a sentence naming what's missing.
112
+ If they aren't there, say so; don't invent values.
113
+
114
+ ## 3. The page component
115
+
116
+ **Reuse the site's own layout.** `src/components/caracms/Page.astro` renders the layout
117
+ the old routes used (`src/layouts/…`), with `<Head>` in its head and `<Blocks>` where the
118
+ page's content went (see `caracms-blocks` → The page component). Give the layout what it
119
+ needs to stay usable by the code routes too: its own `<title>` and description only when
120
+ no `<Head>` is passed (a slot or an optional prop), so a document page has one of each.
121
+ Its markup, classes, CSS imports and scripts stay as they are. Set `<html data-preview>`
122
+ from `preview`.
123
+
124
+ **Keep the old title format.** `<Head>` titles a page `<document title> · <brand name>`.
125
+ If the old titles used another separator (`About | Fernhill`), set the config's
126
+ `titleSeparator: ' | '` (spaces included; see `caracms-blocks` → The page component), not a
127
+ Site Settings value: the format is design. Code routes that build their own `<title>` use
128
+ `settings.seo.titleSeparator` and `settings.brand.name` from `siteSettings` (section 7), so
129
+ every title matches. Check each page's `<title>` line in look-same's HTML diff (section 9).
130
+
131
+ How `<Head>` chooses a title:
132
+
133
+ 1. the page's own `seo.title`, exactly as written;
134
+ 2. otherwise, on the home page only, Site Settings' `seo.title`, then the brand's name,
135
+ with no separator (the document's `title` only when both are empty);
136
+ 3. otherwise `<document title><titleSeparator><brand name>`.
137
+
138
+ So a page whose old title was `<page> | <brand>` drops its `seo.title` and lets step 3
139
+ build it (give the document the old `<page>` part as its `title`). A home title in another
140
+ form, such as `<brand> | <tagline>`, goes in the home page's own `seo.title`
141
+ (`content/pages/index.json`), not Site Settings'. That way the home page's title sits with
142
+ the home page, where an editor changing it will look, and it wins whatever Site Settings
143
+ holds.
144
+
145
+ If the old site showed one site-wide title on every page without its own (a Tina template
146
+ does), set the config's `titleFallback: 'site'` beside `titleSeparator`: step 2 then holds
147
+ on every page, not only the home page (the paged-listing ` — page N` still follows). The
148
+ default, `'page-and-brand'`, is the list above.
149
+
150
+ **Per-page layout props.** The old routes often passed the layout something by hand: the
151
+ header's colour or variant, `<main>`'s classes, a script in `<head>` for one page, the
152
+ current page for the nav's highlight. A document has no route to do that, so the page
153
+ component works them out:
154
+
155
+ - from the page's address, when it's about where the page is (the nav's current item, a
156
+ section of the site): `const { path } = Astro.props`, the document's address (`/about`),
157
+ the same on the public page and in the preview, and null when the document has no page.
158
+ Never read `Astro.url` or `?path=` for it: in the preview they're the editor's own
159
+ address. Pass it on to `<Head path={path}>` as `init`'s page component does;
160
+
161
+ - from a page field, when an editor might choose it: give the `pages` collection
162
+ `fields` (`fields: { footer: select({ options: […] }) }`) and read it from
163
+ `Astro.props.data`;
164
+ - from the page's first block, when it follows from how the page opens:
165
+
166
+ ```astro
167
+ ---
168
+ const LOOKS: Record<string, { header: string; mainClass?: string; intro?: boolean }> = {
169
+ homeHero: { header: 'blue', mainClass: 'home-page', intro: true },
170
+ aboutHero: { header: 'ink', mainClass: 'bg-ink text-white' },
171
+ };
172
+ const first = 'blocks' in document ? document.blocks[0]?.type : undefined;
173
+ const look = (first && LOOKS[first]) || { header: 'ink' };
174
+ ---
175
+ ```
176
+
177
+ A head script that plays an intro or hides content goes in only when `!preview`: the
178
+ editor wants the settled page.
179
+
180
+ ## 4. Convert page by page
181
+
182
+ For each page, in one commit:
183
+
184
+ 1. **Its sections become blocks.** Each section component becomes a block: a config entry
185
+ (`caracms.config.ts` → `blocks`) with a field for each thing an editor would change,
186
+ and a component that renders **the same elements and classes** as before, with the
187
+ text drawn through `Text` / `RichText` / `Lockup`, links through `Link` / `Buttons` and
188
+ images through `Media` (use `as` and `class` to keep the old elements; `Text` always
189
+ renders one, so pick the element the old markup had, `as="span"` inside a sentence).
190
+ Attributes pass through as written (`<Block data-hero>` renders bare). In a component
191
+ slot write `{label}{" "}<span>`, not `{label} <span>`: Astro's compiler drops that
192
+ space (see `caracms-blocks` → A block component). Reuse a block
193
+ across pages when the sections are the same; don't make one block per page. Things
194
+ that aren't content (an animation's settings, a decorative SVG) stay in the component.
195
+ 2. **Its content becomes a document**: `content/pages/<path>.json` (`index.json` for
196
+ Home), with the page's copy word for word, its `$schema`, a `title`, and its blocks in
197
+ order. Images by key (step 5).
198
+ 3. **Its `.astro` route is deleted** (`src/pages/about.astro` for `/about`,
199
+ `src/pages/index.astro` for Home), in the same change. A page is code or a document,
200
+ never both: while the route exists, its address belongs to it.
201
+ 4. **Lists of pages in the site's code become content.** Search the code for the page's path (`'/about'`).
202
+ A site often lists its pages by hand: a table of each page's colour or header variant,
203
+ a sitemap or page list its tests check, a nav it doesn't read from settings. An editor
204
+ who adds a page in `/admin` can't add it to those, so the new page would fall through to
205
+ a default no one chose. Turn each list into something the page brings with it:
206
+ - a page field, when it's a choice per page (`fields: { ground: select({ options: […] }) }`
207
+ on the `pages` collection, read from `Astro.props.data` in the page component, which
208
+ can write it onto the page for a script to read);
209
+ - the page's first block, when it follows from how the page opens (the `LOOKS` map in
210
+ section 3);
211
+ - the content itself, when it's a list of pages: read the documents
212
+ (`contentReader(site)` from `caracms/content` in a code route, or the files in
213
+ `content/pages/` in a test) rather than a list kept by hand.
214
+
215
+ Give each a default that looks right on a plain new page.
216
+ 5. `npx caracms validate`, `astro build`, the site's own tests (section 2), and the
217
+ look-same check for that page. After a script writes the content files (a conversion
218
+ from the old CMS's), run `npx caracms validate --fix`: it lays each file out as the
219
+ editor saves it (two-space JSON, whitespace only), so an editor's first save doesn't
220
+ reformat the whole file.
221
+
222
+ **Ship a general block.** With the first converted page, add a block any new page can be
223
+ made from, a heading and rich text, even if the site's pages don't need one yet: `init`'s
224
+ `text` block is one plain field, so without it a plain new page (a Careers page, a
225
+ policy) means writing code. Style it with the site's own CSS for headings and paragraphs:
226
+
227
+ ```ts
228
+ // src/components/caracms/prose.ts
229
+ import { richText, text, type BlockConfig, type BlockData } from 'caracms/config';
230
+
231
+ export const prose = {
232
+ label: 'Heading and text',
233
+ component: 'src/components/caracms/ProseBlock.astro',
234
+ fields: {
235
+ heading: text({ heading: true }),
236
+ body: richText({ headings: ['h2', 'h3'] }),
237
+ },
238
+ } satisfies BlockConfig;
239
+
240
+ export type ProseData = BlockData<typeof prose.fields>;
241
+ ```
242
+
243
+ with `prose` in `caracms.config.ts` → `blocks`, and a component that draws `heading`
244
+ through `Text` and `body` through `RichText` (see `caracms-blocks` → A block component).
245
+ Give it a `LOOKS` entry (or whatever the page component reads) so a page that opens with
246
+ it gets a sensible header.
247
+
248
+ **Every block's CSS loads on every document page.** `[...path].astro` serves every
249
+ document, so it imports every block component, and Astro bundles all their CSS for it.
250
+ Astro scopes a component's `<style>` to its own elements (`data-astro-cid-*`), so About's
251
+ styles can't touch Home's blocks; that part is bytes, not looks. caracms's `<Media>` brings
252
+ its `.cms-player` styles the same way. What isn't fine is a block with `<style is:global>`
253
+ or a global stylesheet import: that now applies on every document page, so move it to the
254
+ layout or scope it. A site check that one page's CSS is absent from another can't hold any
255
+ more; change it to check the looks.
256
+
257
+ **The order changes too, and order can decide who wins.** The page's CSS now comes in
258
+ other chunks and another order (a chunk per shared component, every block's styles on
259
+ every page). The cascade settles a tie in specificity by order, so check where a global
260
+ rule (the layout's stylesheet, a reset, Tailwind) and a block's scoped rule set the same
261
+ property on the same element:
262
+
263
+ - **How Astro scopes**: the site's `astro.config` `scopedStyleStrategy`. The default since
264
+ Astro 3 is `'attribute'`: each compound in a scoped selector gets `[data-astro-cid-…]`,
265
+ so `.title` is written out as `.title[data-astro-cid-x]`, one class's worth of
266
+ specificity more per compound (`.a .b` gains two). `'class'` adds a class the same way.
267
+ `'where'` adds `:where(.astro-x)`, which adds no specificity.
268
+ - **With `'attribute'` or `'class'`**, a scoped rule beats a global rule with the same
269
+ selector whatever the order. Order decides only when they tie after the extra: a global
270
+ `.card .title` (two classes) against a block's `.title` (a class plus the attribute),
271
+ `.prose h2` against a block's `h2`, and any selector inside `:global(…)`, which isn't scoped.
272
+ - **With `'where'`**, every scoped selector counts as written, so a global rule with the
273
+ same selector ties, and the one loaded last wins: that's what can change.
274
+ - **Global against global** is all order: a block's `is:global` styles or global import
275
+ against the layout's stylesheet. `!important` and `@layer` (an unlayered rule beats a
276
+ layered one) win regardless of order.
277
+
278
+ **List each page's stylesheet order, old and new.** look-same's HTML diff sorts `<head>`
279
+ (section 9), so it shows which stylesheets changed but not their order. With both sites
280
+ served as builds (section 2; a dev server inlines its CSS), print each page's `<link rel="stylesheet">` and `<style>` tags in the
281
+ order the page loads them:
282
+
283
+ ```sh
284
+ for page in / /about /team; do
285
+ for site in http://localhost:<baseline port> http://localhost:<new port>; do
286
+ echo "== $site$page"
287
+ curl -s "$site$page" | grep -oE '<link[^>]*stylesheet[^>]*>|<style[^>]*>'
288
+ done
289
+ done
290
+ ```
291
+
292
+ Find the global stylesheet in each list (the layout's import, Tailwind). A stylesheet that
293
+ was after it and is now before it, or the other way round, is where a tie can change
294
+ sides: open that chunk (`dist/client/_astro/<name>.css`) and look for the ties above, and
295
+ grep the moved components for `:global(`. Then look at the elements they style in the
296
+ screenshots at both widths. Nothing else tests the order: a tie the screenshots don't show (a hover, a
297
+ state behind a toggle) needs a look by hand, and a note in the pull request.
298
+
299
+ **`draft: true` and similar per-entry markers** (copy that's a placeholder, say) have no
300
+ built-in equivalent: caracms's drafts are unpublished edits, not a flag on a page. If
301
+ something in the site reads the marker, keep it as a `toggle()` field on the collection;
302
+ otherwise drop it. Either way, say so in the pull request.
303
+
304
+ **Code routes.** Every route in `src/pages/` other than caracms's `[...path].astro` (and a
305
+ collection's own route, below) is the site's code, and its address is reserved: a static
306
+ route (`contact.astro`) reserves exactly `/contact`; a route with parameters
307
+ (`work/[slug].astro`) reserves everything under `/work/`. The editor refuses a page there
308
+ ("… is served by the site's own code (…), so a page can't go there."), and `validate`
309
+ reports a document on one as an error, telling you to delete the route if the document
310
+ replaces it.
311
+
312
+ **What stays code:** pages that aren't content: a design-system or style-guide page,
313
+ developer pages, anything driven by logic rather than copy. Leave them as routes. Pages
314
+ can still link to them, and code can keep using the site's Astro content collections
315
+ (`getCollection`) for anything not yet moved.
316
+
317
+ ## 5. Images: `caracms media import`
318
+
319
+ ```sh
320
+ npx caracms media import src/assets/team/ana.jpg src/assets/hero.webp
321
+ ```
322
+
323
+ It puts each file in the media bucket (under `uploads/<hash>/<name>.<ext>`, as the
324
+ editor's uploads are) and prints one JSON array on stdout:
325
+
326
+ ```json
327
+ [{ "file": "src/assets/team/ana.jpg", "key": "uploads/3f9c0a1b2c3d/ana.jpg", "w": 1200, "h": 1500, "existed": false }]
328
+ ```
329
+
330
+ - It reads `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID` and
331
+ `S3_SECRET_ACCESS_KEY` from the environment, else the site's `.env`.
332
+ - The same image again writes nothing and prints its key with `"existed": true`. It never
333
+ deletes anything.
334
+ - JPEG, PNG, WebP, AVIF, HEIC/HEIF, GIF and SVG, up to 40 MB and 50 megapixels. If any file
335
+ is refused (its type or size), none are written. Exit 0 fine, 1 failed, 2 bad usage.
336
+
337
+ In the content an image is `{ "key", "alt", "w", "h" }` from that output, with the old
338
+ page's alt text. Keep `w` and `h`: without them the image renders, but its box isn't kept
339
+ while it loads, and `validate` warns on every image (not a file such as a PDF) that lacks
340
+ them. For images already in the bucket, run `media import` on their files again: it
341
+ uploads nothing and prints `w` and `h`. A crop keeps the centre unless the value has a `focal` point: if the old
342
+ page cropped elsewhere (an `object-position`), set `focal` to match. The images come
343
+ through imgproxy now, so their bytes and sometimes their sub-pixel sizes differ from
344
+ Astro's; their crops and boxes must not.
345
+
346
+ Once a page's images are in the bucket and its content points at them, the old files can
347
+ go only when nothing in the code still uses them.
348
+
349
+ ## 6. Content collections
350
+
351
+ The site's Astro content collections (`src/content.config.ts`) become caracms collections
352
+ when editors should change them:
353
+
354
+ - A collection with pages of its own: `kind: 'fields'` with a `urlBase`, and its route.
355
+ - A collection only other pages show (a team, an FAQ, logos): `kind: 'fields'` with **no
356
+ `urlBase`** and a `component` (the editor's preview of one document). A block shows them
357
+ with `cms.read.documents('<name>')`, which orders them **by title**: keep the old order
358
+ with a `number()` field sorted in the component, or a `list(reference({ to }))` on the
359
+ page. **Take the old order from the old page's HTML, not the data file.** It's often
360
+ implicit: `getCollection` can return a file collection's entries in id (slug) order,
361
+ not the order the YAML lists them. Number the documents in the order the old page
362
+ shows, and check it in look-same's HTML diff.
363
+ - Each entry becomes `content/<name>/<slug>.json`: `$schema`, `title`, then the fields.
364
+ Map each zod field to a caracms field kind; images through `media import`.
365
+ - **A zod rule that refuses a value** (`.refine()`, `.regex()`, a `.min()` on a string)
366
+ has no caracms equivalent: a caracms field can't refuse what an editor types. Write the
367
+ rule into the field's `help` (`text({ help: 'End with a full stop.' })`), and use what
368
+ the field kinds do offer: `select` options instead of a free string, `text({ max })`
369
+ (a soft limit the editor counts, never a refusal).
370
+ - Code that still reads the old collection with `getCollection` keeps working until you
371
+ move it. A code route can read caracms's documents with `contentReader(site)` from
372
+ `caracms/content` (`site` from `virtual:caracms/config`); the page component gets the
373
+ same reader as `Astro.props.read`.
374
+ - Delete the old collection's files and its entry in `content.config.ts` only when nothing
375
+ reads it any more.
376
+
377
+ ## 7. Site Settings
378
+
379
+ Data the whole site uses (the menu, footer links, contact details, social links) becomes
380
+ Site Settings fields: `caracms.config.ts` → `settings: { fields: { … } }`, with the values
381
+ in `content/site-settings.json`. The page component's header and footer read them from
382
+ `Astro.props.settings`. Set `brand.name` to the site's real name. The logo stays the
383
+ site's own file.
384
+
385
+ **A header and footer the code routes share** read the same object there:
386
+ `await siteSettings(site, Astro.locals)`, with `siteSettings` from `caracms/content` and
387
+ `site` from `virtual:caracms/config`. Pass it into the shared component from both sides.
388
+
389
+ **Placeholder links** (`#`) are valid link values; keep them as links. An address-only
390
+ link that isn't a valid address renders as `null`: draw no link, never `href=""`.
391
+
392
+ **A value in both places** (the site's name in `brand.name` and in the old settings file
393
+ that code routes still read for their titles): an editor changing one would leave the
394
+ other behind. Point the code routes at Site Settings with `siteSettings`; where something
395
+ can't move yet, list each duplicate in the pull request.
396
+
397
+ ## 8. Motion under the preview
398
+
399
+ The editor's preview renders the page in iframes. Opening a document loads them fresh;
400
+ each edit morphs the new render into the frame's `<body>` without reloading. A site with GSAP, ScrollTrigger or Lenis has
401
+ to cope (details in `caracms-blocks` → Scripts and the live preview):
402
+
403
+ - Set `<html data-preview>` in the page component, and in the preview leave the scroll
404
+ native (Lenis with `smoothWheel: false`).
405
+ - In the preview, every animation shows its finished state from the first load: take the
406
+ site's settled path (what it does on a back/forward visit or under reduced motion), and
407
+ leave an intro's head script out.
408
+ - Listen for `caracms:rendered` on `document` (fired after each edit's morph, never on a
409
+ load), set up again on the settled path, and `ScrollTrigger.refresh()`.
410
+
411
+ Check it in the editor: `astro dev`, open `/admin` (with no GitHub App configured it runs
412
+ in local mode: no sign-in, and edits save to the files on disk), open a page, change a
413
+ heading, and watch that the preview updates without jumping.
414
+
415
+ ## 9. The look-same check
416
+
417
+ `look-same.mjs` is beside this skill (`.claude/skills/caracms-adopt/look-same.mjs`). It
418
+ screenshots each page of the old site and the new, full page, at 1440 and 390 wide, with
419
+ reduced motion and lazy content scrolled in, and writes a side-by-side report. It also
420
+ diffs each page's HTML. It needs
421
+ `playwright-core` and Google Chrome on the machine. Install the tool in a temporary folder
422
+ **outside the site**, so the site's `package.json` and lockfile don't change:
423
+
424
+ ```sh
425
+ npm i --prefix /tmp/look-same playwright-core
426
+ node .claude/skills/caracms-adopt/look-same.mjs --playwright /tmp/look-same \
427
+ --old http://localhost:<baseline port> --new http://localhost:<new port> \
428
+ --pages /,/about,/team --out /tmp/look-same/report
429
+ ```
430
+
431
+ - `--pages` is a comma list or a file with one path per line. `--widths` defaults to
432
+ `1440,390`. `--out` gets `index.html`, `scores.json`, `shots/` and `html/`. An option
433
+ it doesn't know is refused (exit 1), so a misspelt flag doesn't quietly do nothing.
434
+ - **Any status is compared.** A page that answers 404 (or anything) on both sides is
435
+ compared like any other, so put a path that doesn't exist in the list to check the
436
+ site's 404 page. Two different statuses are a difference (`statusDiffers: true`), not an
437
+ error: both pages are still shot and diffed. Each result has `status: { old, new }`,
438
+ and the report shows it beside the page.
439
+ - **Images**: before each shot it waits for the images that have started loading, up to
440
+ `--image-wait` seconds (default 5). A lazy image that never starts (in a hidden tab
441
+ panel, a closed `<details>`) isn't waited for. A shot that hit the cap is still taken
442
+ and says so: `imageCap` in `scores.json` (per side, how many images were still loading
443
+ and the first few addresses), a line in the report and on the console. Look at that
444
+ shot before trusting its score.
445
+ - Each pair gets `differingShare` (the share of pixels that differ) and `heightDelta`
446
+ (new height minus old, in CSS pixels). It prints one line per page and width.
447
+ - **The HTML diff**, per page: both pages' HTML as the server sends it, one tag or text
448
+ per line, with asset hashes, `data-astro-cid-*`, attribute order and what renders the
449
+ same normalised away (the script's header lists exactly what). `scores.json` → `html`
450
+ has `differingLines` per page; the diff is in `html/<page>.diff` and in `index.html`.
451
+ Point both `--old` and `--new` at **builds**: a dev server's injected scripts and inlined
452
+ CSS would fill the diff.
453
+ - **A port from another CMS** leaves its own attributes and per-render ids, which fill the
454
+ diff with lines that aren't differences. `--ignore-attr <list>` drops attributes by name,
455
+ a comma list where a trailing `*` matches by prefix. `--mask-ids <regex|auto>` writes ids
456
+ drawn at random per render as `[id1]`, `[id2]`, …, and the same in what points at them
457
+ (`for`, `aria-controls`, `aria-labelledby` and the other id lists, `href="#…"`,
458
+ `url(#…)`). The regex is tested against the id's value, unanchored (`'^(st|sp)-'`);
459
+ `auto` masks React's `:r1:` and ids ending in a run of 5 or more letters and digits with
460
+ both in it (`st-a8f3k2`, not `section-12345`). A reference that no longer points at its
461
+ element still shows (`[id2]` against `[missing1]`). For a Tina site:
462
+
463
+ ```sh
464
+ node .claude/skills/caracms-adopt/look-same.mjs --playwright /tmp/look-same \
465
+ --old http://localhost:<baseline port> --new http://localhost:<new port> \
466
+ --pages pages.txt --out /tmp/look-same/report \
467
+ --ignore-attr 'data-tina-*' --mask-ids auto
468
+ ```
469
+ - **An element that varies run to run on the old site itself** (a wordmark fitted to its box
470
+ while the display font loads, so two shots of the same build differ) can't be fixed by
471
+ either site. `--css <rules>` adds the same CSS to both pages just before each shot: hide
472
+ the element or pin its size, e.g.
473
+ `--css '[data-wordmark-wrap]{visibility:hidden!important}'`. It changes the shots only,
474
+ not the HTML diff, and the report names the rules; keep them to the one element, since
475
+ whatever they cover is no longer compared.
476
+ - **Spaces**: a space between inline content shows as `␣` at the start of the next text,
477
+ wherever it sat (`<span>sub </span><em>` and `<span>sub</span> <em>` are the same), and a
478
+ space beside a block tag is dropped, since neither shows. So a `␣` line in the diff is a
479
+ space that was really added or lost.
480
+ - **`<head>`'s children are compared in any order** (caracms writes `<title>` and the
481
+ description in its own place), so a `<head>` line in the diff is a tag that changed or
482
+ is new. Check the `<title>` line on every page: a different separator or brand name
483
+ shows there (section 3). Order in `<body>` is compared.
484
+ - Exit 0 when the report is written (whatever the scores, statuses or image caps), 1 for a
485
+ usage error, 2 when a page couldn't be fetched at all.
486
+ - **Read both.** Screenshots miss what doesn't show: an attribute, a list in the wrong
487
+ order under an image or a cover, a space a font hides (`␣→` in old, `→` in new, after `VIEW ALL`:
488
+ a slot's `{label} <span>` that needs `{label}{" "}<span>`), a link's `href`. The HTML diff misses what CSS does. Expect caracms's own
489
+ `og:` and `twitter:` meta tags in the diff; everything else in it is a difference to fix
490
+ or to explain in the pull request.
491
+ - **The bar:** text, layout and spacing the same: `heightDelta` 0 and a `differingShare`
492
+ near 0. Image encoding and sub-pixel scaling may differ a little; a person judges those
493
+ from `index.html`. Anything else is a difference to fix, not to explain away.
494
+
495
+ ## 10. Before the pull request
496
+
497
+ 1. `npx caracms validate`: no errors (see `caracms-content` → Getting it merged).
498
+ 2. `astro build`: every page builds.
499
+ 3. The site's own tests, as they passed before `init` (section 2).
500
+ 4. The look-same check of every page you converted: screenshots and HTML diff.
501
+ 5. **A new page needs no code.** In `/admin`, add a page made of the general block, then
502
+ look at it in the preview and, after `astro build`, on the built site: its header,
503
+ colours and anything else the site set per page come out right, and the site's own
504
+ tests still pass with it there. Anything that's wrong points at a list still kept in
505
+ code (section 4, step 4: lists of pages). Delete the test page before the pull request.
506
+
507
+ Work on a branch and open a pull request; never commit to `main` directly, and never
508
+ touch the `drafts` branch.