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.
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/bin/caracms.mjs +174 -0
- package/bin/gate.mjs +275 -0
- package/bin/init.mjs +712 -0
- package/bin/media.mjs +662 -0
- package/bin/site-runner.mjs +61 -0
- package/bin/templates/Page.astro.txt +31 -0
- package/bin/templates/TextBlock.astro.txt +13 -0
- package/bin/templates/caracms.config.ts.txt +22 -0
- package/bin/templates/catch-all.astro.txt +25 -0
- package/bin/templates/health.ts.txt +6 -0
- package/bin/templates/railway.ts.txt +147 -0
- package/package.json +66 -4
- package/skills/caracms-adopt/SKILL.md +508 -0
- package/skills/caracms-adopt/look-same.mjs +957 -0
- package/skills/caracms-blocks/SKILL.md +376 -0
- package/skills/caracms-content/SKILL.md +201 -0
- package/skills/caracms-tina/SKILL.md +427 -0
- package/skills/caracms-tina/field-kinds.md +88 -0
- package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
- package/src/block-runs.ts +84 -0
- package/src/cards.ts +158 -0
- package/src/cli/schemas.ts +122 -0
- package/src/cli/site.ts +66 -0
- package/src/cli/validate.ts +471 -0
- package/src/code-routes-disk.ts +67 -0
- package/src/code-routes.ts +134 -0
- package/src/columns.ts +39 -0
- package/src/components/Block.astro +46 -0
- package/src/components/BlockRunTree.astro +45 -0
- package/src/components/Blocks.astro +139 -0
- package/src/components/Buttons.astro +116 -0
- package/src/components/Fonts.astro +14 -0
- package/src/components/Form.astro +221 -0
- package/src/components/Head.astro +62 -0
- package/src/components/Link.astro +68 -0
- package/src/components/Lockup.astro +116 -0
- package/src/components/Media.astro +124 -0
- package/src/components/MediaPlayer.astro +102 -0
- package/src/components/RenderDocument.astro +64 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +81 -0
- package/src/components/attributes.ts +33 -0
- package/src/components/descriptor.ts +45 -0
- package/src/components/form-client.ts +182 -0
- package/src/components/media-player.ts +46 -0
- package/src/components/media-props.ts +35 -0
- package/src/components/sitemap.xml.ts +36 -0
- package/src/config.ts +825 -0
- package/src/content-schema.ts +166 -0
- package/src/content.ts +422 -0
- package/src/document.ts +579 -0
- package/src/editor/README.md +405 -0
- package/src/editor/addresses.ts +61 -0
- package/src/editor/brand.ts +118 -0
- package/src/editor/client/block-list/BlockList.tsx +342 -0
- package/src/editor/client/block-list/Picker.tsx +105 -0
- package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
- package/src/editor/client/block-list/insert-points.ts +56 -0
- package/src/editor/client/block-list/link.ts +71 -0
- package/src/editor/client/block-list/reorder.ts +105 -0
- package/src/editor/client/block-list/summary.ts +118 -0
- package/src/editor/client/collections/CollectionGrid.tsx +143 -0
- package/src/editor/client/collections/CollectionTable.tsx +406 -0
- package/src/editor/client/collections/table.ts +270 -0
- package/src/editor/client/context.tsx +27 -0
- package/src/editor/client/documents/DocMenu.tsx +154 -0
- package/src/editor/client/documents/actions.ts +192 -0
- package/src/editor/client/documents/dialogs.tsx +687 -0
- package/src/editor/client/editor.ts +995 -0
- package/src/editor/client/fields/ButtonsField.tsx +87 -0
- package/src/editor/client/fields/DateField.tsx +200 -0
- package/src/editor/client/fields/FieldControl.tsx +61 -0
- package/src/editor/client/fields/FieldFrame.tsx +109 -0
- package/src/editor/client/fields/GroupField.tsx +113 -0
- package/src/editor/client/fields/HiddenToggle.tsx +51 -0
- package/src/editor/client/fields/LinkField.tsx +307 -0
- package/src/editor/client/fields/ListField.tsx +642 -0
- package/src/editor/client/fields/LockupField.tsx +365 -0
- package/src/editor/client/fields/MediaField.tsx +823 -0
- package/src/editor/client/fields/PageFields.tsx +82 -0
- package/src/editor/client/fields/ReferenceField.tsx +209 -0
- package/src/editor/client/fields/RichTextField.tsx +306 -0
- package/src/editor/client/fields/SelectField.tsx +63 -0
- package/src/editor/client/fields/SeoGroup.tsx +282 -0
- package/src/editor/client/fields/SimpleFields.tsx +89 -0
- package/src/editor/client/fields/TextField.tsx +51 -0
- package/src/editor/client/fields/date.ts +73 -0
- package/src/editor/client/fields/host.tsx +172 -0
- package/src/editor/client/fields/list-summary.ts +85 -0
- package/src/editor/client/fields/lockup-rules.ts +181 -0
- package/src/editor/client/fields/parts.tsx +458 -0
- package/src/editor/client/fields/rich-text-commands.ts +183 -0
- package/src/editor/client/fields/rich-text-doc.ts +156 -0
- package/src/editor/client/fields/rich-text-schema.ts +224 -0
- package/src/editor/client/fields/stale.ts +58 -0
- package/src/editor/client/inbox/InboxStage.tsx +272 -0
- package/src/editor/client/inbox/store.ts +311 -0
- package/src/editor/client/live/format.ts +77 -0
- package/src/editor/client/live/marks.tsx +90 -0
- package/src/editor/client/live/notices.tsx +213 -0
- package/src/editor/client/live/publish.tsx +617 -0
- package/src/editor/client/live/session.tsx +162 -0
- package/src/editor/client/main.tsx +12 -0
- package/src/editor/client/media/LibraryStage.tsx +75 -0
- package/src/editor/client/media/ReplaceDialog.tsx +355 -0
- package/src/editor/client/media/store.ts +618 -0
- package/src/editor/client/nav/DocRow.tsx +122 -0
- package/src/editor/client/nav/search.ts +71 -0
- package/src/editor/client/nav/tree.ts +93 -0
- package/src/editor/client/overlays/OverlayMount.tsx +26 -0
- package/src/editor/client/overlays/editor-marks.ts +159 -0
- package/src/editor/client/overlays/index.ts +50 -0
- package/src/editor/client/overlays/layer.ts +437 -0
- package/src/editor/client/overlays/overlay-css.ts +177 -0
- package/src/editor/client/overlays/rich-text-bar.ts +717 -0
- package/src/editor/client/pane/Pane.tsx +91 -0
- package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
- package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
- package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
- package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
- package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
- package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
- package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
- package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
- package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
- package/src/editor/client/pane/parts.tsx +179 -0
- package/src/editor/client/pane/switcher.tsx +165 -0
- package/src/editor/client/preview/driver.ts +640 -0
- package/src/editor/client/preview/field-text.ts +71 -0
- package/src/editor/client/preview/follow-links.ts +234 -0
- package/src/editor/client/preview/layout.ts +37 -0
- package/src/editor/client/preview/link-target.ts +50 -0
- package/src/editor/client/preview/markers.ts +86 -0
- package/src/editor/client/preview/page-edit.ts +632 -0
- package/src/editor/client/preview/page-editor-registry.ts +63 -0
- package/src/editor/client/preview/page-editor.ts +235 -0
- package/src/editor/client/preview/render-sequencer.ts +113 -0
- package/src/editor/client/preview/selection.ts +116 -0
- package/src/editor/client/preview/warnings.ts +61 -0
- package/src/editor/client/shell/App.tsx +101 -0
- package/src/editor/client/shell/Bar.tsx +197 -0
- package/src/editor/client/shell/Brand.tsx +33 -0
- package/src/editor/client/shell/Icons.tsx +87 -0
- package/src/editor/client/shell/NarrowNote.tsx +47 -0
- package/src/editor/client/shell/Popovers.tsx +47 -0
- package/src/editor/client/shell/Stage.tsx +132 -0
- package/src/editor/client/shell/Toast.tsx +61 -0
- package/src/editor/client/state/document-store.ts +465 -0
- package/src/editor/client/state/keeper.ts +713 -0
- package/src/editor/client/state/live.ts +257 -0
- package/src/editor/client/state/schema.ts +42 -0
- package/src/editor/client/state/store.ts +39 -0
- package/src/editor/fonts/OFL-GeistMono.txt +93 -0
- package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
- package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/src/editor/middleware.ts +62 -0
- package/src/editor/routes/_live.ts +26 -0
- package/src/editor/routes/admin.astro +80 -0
- package/src/editor/routes/create.ts +19 -0
- package/src/editor/routes/delete.ts +19 -0
- package/src/editor/routes/document.ts +39 -0
- package/src/editor/routes/documents.ts +30 -0
- package/src/editor/routes/github.ts +23 -0
- package/src/editor/routes/history.ts +9 -0
- package/src/editor/routes/lock.ts +9 -0
- package/src/editor/routes/login/assets.d.ts +10 -0
- package/src/editor/routes/login/login.ts +43 -0
- package/src/editor/routes/media.ts +16 -0
- package/src/editor/routes/move.ts +17 -0
- package/src/editor/routes/notice.ts +9 -0
- package/src/editor/routes/preview.astro +77 -0
- package/src/editor/routes/publish.ts +9 -0
- package/src/editor/routes/status.ts +9 -0
- package/src/editor/routes/submissions.ts +16 -0
- package/src/editor/routes/try-again.ts +9 -0
- package/src/editor/routes/write.ts +18 -0
- package/src/editor/server/admin-props.ts +151 -0
- package/src/editor/server/cards.ts +61 -0
- package/src/editor/server/create.ts +414 -0
- package/src/editor/server/drafts-store.ts +127 -0
- package/src/editor/server/media/alt.ts +107 -0
- package/src/editor/server/media/bucket.ts +295 -0
- package/src/editor/server/media/config.ts +78 -0
- package/src/editor/server/media/handler.ts +221 -0
- package/src/editor/server/media/library.ts +254 -0
- package/src/editor/server/media/memory.ts +184 -0
- package/src/editor/server/media/mux.ts +241 -0
- package/src/editor/server/media/sniff.ts +599 -0
- package/src/editor/server/media/upload.ts +175 -0
- package/src/editor/server/mode.ts +63 -0
- package/src/editor/server/move.ts +295 -0
- package/src/editor/server/render.tsx +9 -0
- package/src/editor/server/replace.ts +72 -0
- package/src/editor/server/settings.ts +35 -0
- package/src/editor/server/write.ts +366 -0
- package/src/editor/styles/editor.css +502 -0
- package/src/editor/styles/fonts.css +40 -0
- package/src/editor/styles/levels/README.md +4 -0
- package/src/editor/styles/levels/collections.css +99 -0
- package/src/editor/styles/levels/document-level.css +10 -0
- package/src/editor/styles/levels/documents.css +9 -0
- package/src/editor/styles/levels/fields.css +171 -0
- package/src/editor/styles/levels/files.css +95 -0
- package/src/editor/styles/levels/live.css +10 -0
- package/src/editor/styles/levels/m5a-documents.css +51 -0
- package/src/editor/styles/levels/m6.css +82 -0
- package/src/editor/styles/levels/media.css +175 -0
- package/src/editor/styles/levels/switcher.css +3 -0
- package/src/editor/styles/tokens.css +114 -0
- package/src/editor/tsconfig.json +16 -0
- package/src/editor/types.ts +881 -0
- package/src/editor/ui-state.ts +113 -0
- package/src/email.ts +67 -0
- package/src/fields.ts +1129 -0
- package/src/files-route.ts +201 -0
- package/src/files.ts +116 -0
- package/src/forms/email.ts +149 -0
- package/src/forms/endpoint.ts +347 -0
- package/src/forms/inbox.ts +273 -0
- package/src/forms/rate-limit.ts +130 -0
- package/src/forms/route.ts +24 -0
- package/src/forms/rules.ts +278 -0
- package/src/forms/store.ts +486 -0
- package/src/head.ts +125 -0
- package/src/index.ts +434 -0
- package/src/json-source.ts +116 -0
- package/src/links.ts +121 -0
- package/src/live/README.md +43 -0
- package/src/live/auth/allow-rules.ts +70 -0
- package/src/live/auth/allow.ts +128 -0
- package/src/live/auth/auth.ts +155 -0
- package/src/live/auth/email.ts +84 -0
- package/src/live/auth/guard.ts +111 -0
- package/src/live/auth/index.ts +68 -0
- package/src/live/auth/links.ts +101 -0
- package/src/live/auth/login.ts +245 -0
- package/src/live/auth/middleware.ts +53 -0
- package/src/live/auth/request.ts +56 -0
- package/src/live/auth/routes.ts +9 -0
- package/src/live/auth/session.ts +181 -0
- package/src/live/auth/views.ts +355 -0
- package/src/live/autosaves.ts +306 -0
- package/src/live/backend.ts +652 -0
- package/src/live/branches.ts +19 -0
- package/src/live/drafts-store.ts +591 -0
- package/src/live/editor.ts +465 -0
- package/src/live/git.ts +1021 -0
- package/src/live/github.ts +271 -0
- package/src/live/locals.ts +24 -0
- package/src/live/locks.ts +273 -0
- package/src/live/merge.ts +197 -0
- package/src/live/messages.ts +151 -0
- package/src/live/mode.ts +210 -0
- package/src/live/redis.ts +533 -0
- package/src/live/runtime.ts +164 -0
- package/src/live/status.ts +109 -0
- package/src/live/sweep.ts +228 -0
- package/src/live/timings.ts +47 -0
- package/src/live/webhook.ts +39 -0
- package/src/lockup-css.ts +58 -0
- package/src/measure.ts +145 -0
- package/src/media-value.ts +239 -0
- package/src/media.ts +255 -0
- package/src/read.ts +256 -0
- package/src/redirect-middleware.ts +49 -0
- package/src/redirects.ts +76 -0
- package/src/relink.ts +293 -0
- package/src/reserved.ts +24 -0
- package/src/rich-text.ts +690 -0
- package/src/schema-key.ts +63 -0
- package/src/seo.ts +124 -0
- package/src/serialize.ts +697 -0
- package/src/skipped.ts +35 -0
- package/src/slug.ts +121 -0
- package/src/text-styles.ts +62 -0
- package/src/virtual.d.ts +28 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caracms-blocks
|
|
3
|
+
description: Build or change a caracms site's code — blocks and their fields, collections (with or without pages), text styles and lockups, Site Settings fields, the page component, forms, and how the site's scripts (GSAP, Lenis, carousels) behave in the editor's live preview. Use when editing caracms.config.ts or the Astro components it names.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building blocks and collections
|
|
7
|
+
|
|
8
|
+
`caracms.config.ts` is everything caracms knows about the site. It's plain data: fields
|
|
9
|
+
are made with helpers from `caracms/config`, and components are named by their path from
|
|
10
|
+
the site's root (strings, never imports). `astro.config` passes it to the integration
|
|
11
|
+
(`integrations: [caracms(site)]`). A mistake in it stops `astro dev` and `astro build`
|
|
12
|
+
with a sentence naming the problem.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineSite, list, richText, text } from 'caracms/config';
|
|
16
|
+
|
|
17
|
+
export default defineSite({
|
|
18
|
+
collections: {
|
|
19
|
+
pages: { kind: 'sections', folder: 'content/pages', urlBase: '/', nested: true },
|
|
20
|
+
},
|
|
21
|
+
blocks: {
|
|
22
|
+
faq: {
|
|
23
|
+
label: 'Questions',
|
|
24
|
+
component: 'src/components/caracms/Faq.astro',
|
|
25
|
+
fields: {
|
|
26
|
+
heading: text({ heading: true }),
|
|
27
|
+
items: list({ question: text(), answer: richText() }, { itemLabel: 'question' }),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
textStyles: {},
|
|
32
|
+
page: 'src/components/caracms/Page.astro',
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A block's `type` in the content is its key here, so **never rename a block key or a field
|
|
37
|
+
key** once content uses it: the content would stop matching. Add new ones instead.
|
|
38
|
+
|
|
39
|
+
After changing the config, run `npx caracms schemas` (or `validate --fix`) so `schemas/`
|
|
40
|
+
and the content's `$schema` keys match, and commit `schemas/` with the change.
|
|
41
|
+
`astro dev` rewrites the schema files when it starts or restarts, but never touches
|
|
42
|
+
content.
|
|
43
|
+
|
|
44
|
+
## Fields
|
|
45
|
+
|
|
46
|
+
All from `caracms/config`. Every one takes `label` (default: the key, title-cased) and
|
|
47
|
+
`help` (a line under the field in the editor).
|
|
48
|
+
|
|
49
|
+
| Helper | Options | The component gets |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `text()` | `multiline`, `heading` (it can hold the page's `h1`), `max` (a soft limit) | `string` |
|
|
52
|
+
| `lockup({ style })` | `style`: a key of `textStyles`; `heading` (default true) | `{ lines, phone }` |
|
|
53
|
+
| `select({ options })` | `options`: strings or `{ value, label }`; `default` (else the first) | the value |
|
|
54
|
+
| `richText()` | `headings: ['h2']` or `['h2', 'h3']` (default none); `links` (default true) | rich text, for `<RichText>` |
|
|
55
|
+
| `link()` | `addressOnly: true` for an address with no words | `{ label, href }`, or `{ href }` / `null` |
|
|
56
|
+
| `buttons({ styles })` | `styles` (strings or `{ value, label }`), `max` (default 2), `allowNoLink` | `[{ label, href, style }]` |
|
|
57
|
+
| `toggle()` | `default` | `boolean` |
|
|
58
|
+
| `number()` | `min`, `max`, `step` | `number \| null` |
|
|
59
|
+
| `date()` | | `"YYYY-MM-DD"` or `null` |
|
|
60
|
+
| `media()` | `only: 'image'` or `'video'`; `play: 'loop'` | for `<Media>` |
|
|
61
|
+
| `reference({ to })` | `to`: a collection's key | the document's path, or `null` if it's gone |
|
|
62
|
+
| `list(field)` / `list({ … })` | `itemLabel`, `min`, `max` (warned, never enforced) | an array |
|
|
63
|
+
| `group({ … })` | | one object |
|
|
64
|
+
|
|
65
|
+
- A list of several fields may hold one list among its fields; that inner list can't hold
|
|
66
|
+
a list. A group can't hold a group.
|
|
67
|
+
- Type a component's data from the schema:
|
|
68
|
+
`export type FaqData = BlockData<typeof faq.fields>;` (keep the block's config in a
|
|
69
|
+
`.ts` beside its component and import it into `caracms.config.ts`).
|
|
70
|
+
|
|
71
|
+
## A block component
|
|
72
|
+
|
|
73
|
+
```astro
|
|
74
|
+
---
|
|
75
|
+
import Block from 'caracms/components/Block.astro';
|
|
76
|
+
import RichText from 'caracms/components/RichText.astro';
|
|
77
|
+
import Text from 'caracms/components/Text.astro';
|
|
78
|
+
import type { BlockProps } from 'caracms/document';
|
|
79
|
+
import type { FaqData } from './faq.ts';
|
|
80
|
+
|
|
81
|
+
type Props = BlockProps<FaqData>;
|
|
82
|
+
const { block, cms } = Astro.props;
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<Block cms={cms} class="faq">
|
|
86
|
+
<Text cms={cms} field="heading" value={block.heading} class="faq-title" />
|
|
87
|
+
{block.items.map((item, i) => (
|
|
88
|
+
<details>
|
|
89
|
+
<summary><Text cms={cms} field={`items.${i}.question`} value={item.question} as="h3" /></summary>
|
|
90
|
+
<RichText cms={cms} field={`items.${i}.answer`} value={item.answer} class="prose" />
|
|
91
|
+
</details>
|
|
92
|
+
))}
|
|
93
|
+
</Block>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Wrap the block in `<Block cms={cms}>`** (`as` sets the element, default `section`;
|
|
97
|
+
`class` and other attributes pass through). In the editor's preview it marks the block
|
|
98
|
+
so it can be selected.
|
|
99
|
+
- **Attributes pass through as on a plain element**, on `Block`, `Text`, `RichText`,
|
|
100
|
+
`Lockup`, `Buttons`, `Link` and `Media`: a bare `data-hero` renders bare,
|
|
101
|
+
`hidden={false}` renders nothing. `aria-*`, `draggable`, `contenteditable`,
|
|
102
|
+
`spellcheck` and `writingsuggestions` keep their `"true"` / `"false"` values.
|
|
103
|
+
- **Whitespace in a slot**: Astro's compiler drops the space between an expression and an
|
|
104
|
+
element in any component's slot, so `<Link …>{label} <span>→</span></Link>` loses it.
|
|
105
|
+
Write `{label}{" "}<span>→</span>`.
|
|
106
|
+
- **Draw every field through the package's component for its kind**, with `field` (its
|
|
107
|
+
key, or a dotted path into a list: `items.0.question`) and `value`. In the preview they
|
|
108
|
+
carry the markers that make text editable on the page; on the public page they're plain
|
|
109
|
+
markup. Each takes `class` and `as` (the element) and passes other attributes on.
|
|
110
|
+
- `Text`: a `<p>`, or `h1`/`h2` for a `heading: true` field (the page's first heading is
|
|
111
|
+
the `h1`). `as` overrides that. It always renders its element, since the editor
|
|
112
|
+
needs one to edit, so text that was a bare text node gains one: use the element the
|
|
113
|
+
old markup had where it had one (`as="span"` inside a sentence).
|
|
114
|
+
- `Lockup`: a lockup in its text style, fitted to its box.
|
|
115
|
+
- `RichText`: a `<div>` of the rich text's HTML.
|
|
116
|
+
- `Link`: an `<a>` holding the label (or what you put inside `<Link>`); a link with no
|
|
117
|
+
label or no address draws nothing. An address-only link wraps what's inside it (an
|
|
118
|
+
image, an icon). An address-only link's value is `{ href }`, or `null` when it's
|
|
119
|
+
empty or isn't a valid address; handle `null` in your own markup (draw no link), never
|
|
120
|
+
fall back to `href=""`. A valid address is a `/path`, `https://`, `mailto:`, `tel:`,
|
|
121
|
+
`#id`, or `#` alone.
|
|
122
|
+
- `Buttons`: the buttons, each with `buttonClass` and `<buttonClass>--<style>`.
|
|
123
|
+
- `Media`: exactly one `<img>` (or the clip). Pass `sizes` (and optionally `ratio`,
|
|
124
|
+
width / height) for a fluid image, or a CSS `width` and/or `height` for a fixed one;
|
|
125
|
+
`fit: 'contain'` to fit instead of crop. The block's CSS sizes it through `class`.
|
|
126
|
+
It takes the element's own attributes (`id`, `title`, `draggable`, `aria-*`,
|
|
127
|
+
`data-*`) too. With an empty value it draws nothing for visitors and, in a preview,
|
|
128
|
+
a dashed placeholder the editor can click to pick the image. So call it as
|
|
129
|
+
`(block.media || cms.preview) && <Media …/>`, not `block.media && …`, wherever an
|
|
130
|
+
empty slot should still be reachable on the page.
|
|
131
|
+
- **A media value reaches the block resolved, not as stored.** Where the file has
|
|
132
|
+
`{ key, alt, focal, w, h }` or `{ video, play, poster: 12.5 }`, the block gets
|
|
133
|
+
`{ kind: 'image', key, alt, decorative, focal, position, w, h }` or
|
|
134
|
+
`{ kind: 'video', playbackId, play, name, mp4, poster: <url>, duration, w, h }`
|
|
135
|
+
(`poster` is Mux's still as a URL, `w`, `h` and `duration` are numbers or `null`;
|
|
136
|
+
`block.media` is `null` when the slot is empty). A site helper that reads a media
|
|
137
|
+
value (a hero's poster, a video's `playbackId`) reads this shape; use `kind` to tell
|
|
138
|
+
the two apart.
|
|
139
|
+
- `cms` is the render context: `cms.preview` (a preview render), `cms.first`,
|
|
140
|
+
`cms.index`, `cms.read` (other documents, below). For "the first block on the page"
|
|
141
|
+
(an eager hero, a heading that opens the page) use `cms.first`, true for the first
|
|
142
|
+
block that renders. Not `cms.index === 0`: `index` is the entry's place in the
|
|
143
|
+
document's `blocks`, counting entries that don't render (an unknown type, a shared block
|
|
144
|
+
that's gone). `headingTag(cms, 'heading')` from
|
|
145
|
+
`caracms/document` gives `'h1'` or `'h2'` for markup of your own.
|
|
146
|
+
- In a preview, draw empty fields too (`cms.preview || block.heading`), so an editor can
|
|
147
|
+
click into them.
|
|
148
|
+
|
|
149
|
+
## Collections
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// A collection with pages of its own, at /work/<slug>
|
|
153
|
+
work: { kind: 'fields', folder: 'content/work', urlBase: '/work/', component: 'src/components/caracms/CaseStudy.astro', fields: { … } },
|
|
154
|
+
// A collection with no pages (a team, an FAQ): leave out urlBase
|
|
155
|
+
people: { kind: 'fields', folder: 'content/people', component: 'src/components/caracms/Person.astro', fields: { role: text(), photo: media({ only: 'image' }) } },
|
|
156
|
+
// A second section builder (a journal)
|
|
157
|
+
journal: { kind: 'sections', folder: 'content/journal', urlBase: '/journal/', fields: { date: date() }, blocks: ['text', 'image'] },
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- A `fields` collection's `component` renders one document: it gets
|
|
161
|
+
`{ document, cms }` (`FieldsDocumentProps` from `caracms/document`), with
|
|
162
|
+
`document.title` and each field's value; draw fields with `field="<key>"`. The page
|
|
163
|
+
component's `<Blocks>` renders it. For a pageless collection that's only the editor's
|
|
164
|
+
preview of one document.
|
|
165
|
+
- `title` is every document's own; no field can be called `title` (or, on a `sections`
|
|
166
|
+
collection, `blocks` or `seo`).
|
|
167
|
+
- Options: `label`, `columns` and `sort` (the editor's table), `view`
|
|
168
|
+
(`{ default: 'list' | 'grid' }`), and on a `sections` collection `fields` (page fields)
|
|
169
|
+
and `blocks` (the block types its pages may use).
|
|
170
|
+
- **Show a pageless collection** from a block: `await cms.read.documents('people')` gives
|
|
171
|
+
every document as `{ path, title, data }` (`data` is its fields' values, as a component
|
|
172
|
+
gets them), **ordered by title**. For another order, give the documents a `number()`
|
|
173
|
+
field and sort in the component, or let a page pick and order them with
|
|
174
|
+
`list(reference({ to: 'people' }))`. `cms.read.document('people', path)` reads one.
|
|
175
|
+
- The collection's route: a collection with a `urlBase` needs a route in `src/pages/` whose
|
|
176
|
+
static part is that `urlBase` (`src/pages/work/[slug].astro`, rendering with
|
|
177
|
+
`RenderDocument` from `caracms/components/RenderDocument.astro` and `listDocuments` /
|
|
178
|
+
`readDocument` from `caracms/content`, as `src/pages/[...path].astro` does for pages).
|
|
179
|
+
Its `getStaticPaths` leaves out entries with a `warning` (a file that can't be opened) and
|
|
180
|
+
`hidden` ones (a page hidden from the site, or one under it): those answer the 404 page.
|
|
181
|
+
Every other route in `src/pages/` is the site's own code, and its address can't hold a
|
|
182
|
+
document.
|
|
183
|
+
- Shared blocks (`content/shared/`) and Site Settings are built in; no config.
|
|
184
|
+
|
|
185
|
+
## Site Settings fields
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
settings: { fields: { menu: list({ label: text(), href: link({ addressOnly: true }) }), phone: text() } },
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
They're edited under Site Settings and saved in `content/site-settings.json`, beside the
|
|
192
|
+
package's own keys (`title`, `editors`, `brand`, `seo`, `googleTag`, `formRecipients`,
|
|
193
|
+
`redirects`, which a field can't be called). The page component gets them as
|
|
194
|
+
`Astro.props.settings` (plus `settings.brand.name`): the header, the footer and other
|
|
195
|
+
parts of the page around the blocks read them there. A preview shows the draft.
|
|
196
|
+
|
|
197
|
+
The site's own `.astro` routes get the same object from `siteSettings`, so a header and
|
|
198
|
+
footer shared with them read one source:
|
|
199
|
+
|
|
200
|
+
```astro
|
|
201
|
+
---
|
|
202
|
+
import { siteSettings } from 'caracms/content';
|
|
203
|
+
import { site } from 'virtual:caracms/config';
|
|
204
|
+
const settings = await siteSettings(site, Astro.locals);
|
|
205
|
+
---
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**A block that needs Site Settings** (a form block showing the site's phone number) reads
|
|
209
|
+
them the same way, with `siteSettings(site, Astro.locals)`: in a preview it reads the
|
|
210
|
+
editor's draft, so an edit to Site Settings shows in the block too. `cms.read` is for
|
|
211
|
+
collections only; it doesn't reach Site Settings.
|
|
212
|
+
|
|
213
|
+
## Text styles and lockups
|
|
214
|
+
|
|
215
|
+
A lockup is a heading whose lines are set by the editor and fitted to their box in a
|
|
216
|
+
text style. A style, in `textStyles`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
display: {
|
|
220
|
+
family: 'Site Display', cssVariable: '--font-display',
|
|
221
|
+
fontFile: { measure: 'src/fonts/Display.ttf', web: 'src/fonts/Display.woff2' },
|
|
222
|
+
weightRange: '400 700', axes: { wght: 600 },
|
|
223
|
+
sizePx: 96, minSizePx: 40, trackingEm: -0.02, leading: 1, textTransform: 'none',
|
|
224
|
+
},
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`measure` must be a TTF or OTF; `axes` pins every variable axis (`{}` for a static font).
|
|
228
|
+
`<Fonts />` in the page's `<head>` loads the styles' fonts. `phoneLinesWidth` (default
|
|
229
|
+
640) is where a lockup's phone lines take over. A plain heading that doesn't need fitting
|
|
230
|
+
is simpler as `text({ heading: true })` in the site's own CSS.
|
|
231
|
+
|
|
232
|
+
## The page component
|
|
233
|
+
|
|
234
|
+
The config's `page` renders a whole page from a document, for the public site and the
|
|
235
|
+
editor's preview alike. It gets `PageProps` (`caracms/document`): `document`,
|
|
236
|
+
`collection`, `path`, `preview`, `data` (the document's own fields), `settings`,
|
|
237
|
+
`listPage`, `read`.
|
|
238
|
+
|
|
239
|
+
```astro
|
|
240
|
+
<html lang="en" data-preview={preview ? '' : undefined}>
|
|
241
|
+
<head>
|
|
242
|
+
<Head document={document} collection={collection} path={path} preview={preview} listPage={listPage} />
|
|
243
|
+
<Fonts />
|
|
244
|
+
</head>
|
|
245
|
+
<body>
|
|
246
|
+
<SiteHeader items={settings.menu} />
|
|
247
|
+
<main><Blocks document={document} collection={collection} preview={preview} listPage={listPage} /></main>
|
|
248
|
+
<SiteFooter />
|
|
249
|
+
</body>
|
|
250
|
+
</html>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- `path` (`string | null`) is the document's address (`/`, `/about`,
|
|
254
|
+
`/work/harbour-coffee`), the same on the public page and in the editor's preview: no
|
|
255
|
+
trailing slash, not URL-encoded. Page 2 of a paged listing (`/journal/page/2`) gives its
|
|
256
|
+
document's `/journal` (`listPage` says which page). It's null when the document has no
|
|
257
|
+
page of its own (a shared block's preview, a collection without pages). Read it as
|
|
258
|
+
`const { path } = Astro.props`, never from `Astro.url` or `?path=`: in the preview those
|
|
259
|
+
are the editor's own address. Pass it on as `<Head path={path}>` (it's optional there);
|
|
260
|
+
`caracms init`'s page component does.
|
|
261
|
+
- `<Head>` (`caracms/components/Head.astro`) writes the title, description, canonical and
|
|
262
|
+
share tags. A page's title is its own `seo.title` as written; otherwise, on the home
|
|
263
|
+
page, Site Settings' `seo.title`, then the brand name; otherwise
|
|
264
|
+
`<document title> · <brand name>`. (A home title like `<brand> | <tagline>` goes in the
|
|
265
|
+
home page's own `seo.title`.) The ` · ` is the
|
|
266
|
+
config's `titleSeparator`, spaces included: `titleSeparator: ' | '` beside `page` in
|
|
267
|
+
`defineSite({ … })` keeps a site's old `About | Fernhill`. It's code, not a Site
|
|
268
|
+
Settings field: the title's format is design. `titleFallback: 'site'` beside it makes every
|
|
269
|
+
page without its own `seo.title` fall back as the home page does (Site Settings'
|
|
270
|
+
`seo.title`, then the brand name); the default is `'page-and-brand'`. A code route builds its title the same way
|
|
271
|
+
from `siteSettings`: `` `${title}${settings.seo.titleSeparator}${settings.brand.name}` ``. `<Blocks>` renders the blocks (or a fields document through its
|
|
272
|
+
collection's component).
|
|
273
|
+
- `document.kind === 'shared'` is a shared block's own preview: render it without the
|
|
274
|
+
header and footer.
|
|
275
|
+
- `<Media>` needs a block's `cms`, so images in the page's frame (a logo) stay the site's
|
|
276
|
+
own files.
|
|
277
|
+
|
|
278
|
+
## Forms
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
forms: {
|
|
282
|
+
contact: { label: 'Contact', email: { subject: 'New enquiry' }, fields: {
|
|
283
|
+
name: { label: 'Your name', autocomplete: 'name' },
|
|
284
|
+
email: { label: 'Email', type: 'email' },
|
|
285
|
+
message: { label: 'Message', type: 'textarea', optional: true },
|
|
286
|
+
} },
|
|
287
|
+
},
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Field `type`s: `text` (default), `email`, `tel`, `textarea`, `select` (with `options`),
|
|
291
|
+
`files`. A form's key is stored on every submission, so never rename it once it's live.
|
|
292
|
+
Draw it in a block with `<Form form="contact" preview={cms.preview} submit="Send" />`
|
|
293
|
+
(`caracms/components/Form.astro`); a block can offer a choice with
|
|
294
|
+
`select({ options: [{ value: 'contact', label: 'Contact' }] })`. Submissions land in the
|
|
295
|
+
editor's inbox and are emailed to Site Settings' `formRecipients` for that form.
|
|
296
|
+
|
|
297
|
+
A field can also take `help`, `placeholder` (text, email, tel, textarea), `prefix` (text,
|
|
298
|
+
tel), `error` (its own error line), `autocomplete`, and on a textarea `rows` (its height in
|
|
299
|
+
lines, 1 to 50; default 6).
|
|
300
|
+
|
|
301
|
+
The markup's classes are `.cms-form`, `.cms-form-field`, `.cms-form-label`,
|
|
302
|
+
`.cms-form-help`, `.cms-form-error`, `.cms-form-submit` and `.cms-form-status`. For a
|
|
303
|
+
site that draws its own look, `<Form>` takes, each optional (left out, it renders as
|
|
304
|
+
without them):
|
|
305
|
+
|
|
306
|
+
- `class`, and any other attribute (`data-reveal`, `id`, `aria-*`), on the `<form>`. The
|
|
307
|
+
form's own attributes (`action`, `method`, its `data-caracms-*`…) can't be replaced.
|
|
308
|
+
- `submitClass`: classes on the submit button, beside `cms-form-submit`
|
|
309
|
+
(`submitClass="btn btn-primary"`).
|
|
310
|
+
- `thanks`: the line after a send, in place of "Thanks, we'll be in touch shortly."
|
|
311
|
+
`failed`: the line when a send fails, in place of "Something went wrong. Please email
|
|
312
|
+
us directly." (when the endpoint gives its own sentence, that shows instead).
|
|
313
|
+
- `errors`: each field type's error line, in place of the package's wording:
|
|
314
|
+
`errors={{ email: 'Please check your email address.' }}`. A field's own `error` still wins.
|
|
315
|
+
- `actions`: wraps the button and the status line in `<div class="cms-form-actions">`, so
|
|
316
|
+
the site's CSS can set them in a row.
|
|
317
|
+
- `labelPlaceholders`: a field that can take a placeholder and sets none uses its label.
|
|
318
|
+
|
|
319
|
+
```astro
|
|
320
|
+
<Form form="contact" preview={cms.preview} class="contact-form" data-reveal
|
|
321
|
+
submitClass="btn btn-primary" thanks="Thanks — we'll be in touch." actions labelPlaceholders />
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Scripts and the live preview
|
|
325
|
+
|
|
326
|
+
The editor shows the page in iframes, through the same page component with
|
|
327
|
+
`preview` on. When an editor changes something, the new render is **morphed into the
|
|
328
|
+
frame's `<body>`**: the page isn't reloaded, `<html>` and `<head>` stay, and the body's
|
|
329
|
+
elements are made to match the server's render (so classes and inline styles a script set
|
|
330
|
+
on them can be reset).
|
|
331
|
+
|
|
332
|
+
- **`<html data-preview>`**: set it in the page component from `preview` (as above), so
|
|
333
|
+
scripts can tell: `document.documentElement.hasAttribute('data-preview')`.
|
|
334
|
+
- **Opening another document reloads the frames**, so that page loads as on the public
|
|
335
|
+
site and its own module scripts run on arrival. Only an edit morphs.
|
|
336
|
+
- **`caracms:rendered`**: after each morph (an edit) the frame's `document` gets this
|
|
337
|
+
event; never on a load, which runs the scripts as usual, and never on the public site:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
document.addEventListener('caracms:rendered', () => { /* pick up new elements, refresh */ });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
For motion (GSAP, ScrollTrigger, Lenis, Locomotive), the recipe that works:
|
|
344
|
+
|
|
345
|
+
1. **Read the flag once**, when the site's motion starts:
|
|
346
|
+
`const preview = document.documentElement.hasAttribute('data-preview')`.
|
|
347
|
+
2. **Leave the scroll native in the preview**, since the editor scrolls the frames to the
|
|
348
|
+
block being edited. With Lenis, keep the instance (other code may call it) but turn
|
|
349
|
+
wheel smoothing off: `new Lenis({ smoothWheel: !preview })`.
|
|
350
|
+
3. **Show every animation in its finished state in the preview**, or an editor edits
|
|
351
|
+
blind: no intro, no entrance, nothing hidden until a scroll reveals it. If the site
|
|
352
|
+
already has a settled path (what it does on a back/forward visit, or under
|
|
353
|
+
`prefers-reduced-motion: reduce`), take that path whenever the preview loads (each
|
|
354
|
+
document opened is a fresh load). A head
|
|
355
|
+
script that plays an intro stays out of the preview altogether (the page component
|
|
356
|
+
leaves it out when `preview` is set).
|
|
357
|
+
4. **On `caracms:rendered`, set up again**, on the settled path, then
|
|
358
|
+
`ScrollTrigger.refresh()`. Either set up only the elements that are new and drop
|
|
359
|
+
those that are gone, or, if every setup in the preview is settled, revert them all
|
|
360
|
+
(their GSAP contexts) and run them again: nothing replays, because nothing animates.
|
|
361
|
+
What jumps is rebuilding with the entrances on.
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
const preview = document.documentElement.hasAttribute('data-preview');
|
|
365
|
+
const lenis = new Lenis({ smoothWheel: !preview });
|
|
366
|
+
const settle = preview || matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
367
|
+
runSetups({ settled: settle }); // the site's own: each section's GSAP, in a gsap.context()
|
|
368
|
+
document.addEventListener('caracms:rendered', () => {
|
|
369
|
+
revertSetups();
|
|
370
|
+
runSetups({ settled: true });
|
|
371
|
+
ScrollTrigger.refresh();
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
(`runSetups` and `revertSetups` stand for the site's own code; caracms provides only the
|
|
376
|
+
flag and the event.) With `prefers-reduced-motion: reduce`, show the end state too.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caracms-content
|
|
3
|
+
description: Change a caracms site's content — the JSON files in content/ (pages, collection documents, shared blocks, Site Settings), rich text, images, references, adding an editor — and get it merged with a pull request after `caracms validate`. Use whenever a task adds, edits, moves or deletes content on a site that has caracms.config.ts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Changing content on a caracms site
|
|
7
|
+
|
|
8
|
+
The site's content is JSON files in the repo. The site builds from them, and editors change
|
|
9
|
+
the same files through the editor at `/admin`. What a file may hold is set by
|
|
10
|
+
`caracms.config.ts`: its collections, blocks and fields. Read it first; it's the schema.
|
|
11
|
+
|
|
12
|
+
**Write files the way the editor saves them:** two-space JSON with a trailing newline, one
|
|
13
|
+
member per line, a string array, a media value and each rich-text block on one line of
|
|
14
|
+
their own. Otherwise the editor's next save reformats the whole file. `validate` warns on
|
|
15
|
+
a file that's laid out otherwise, and `validate --fix` rewrites it the editor's way
|
|
16
|
+
(whitespace only: every key stays where it is, every value as it is).
|
|
17
|
+
|
|
18
|
+
## Where things are
|
|
19
|
+
|
|
20
|
+
- **Collections** are in `caracms.config.ts` → `collections`, each with a `folder`.
|
|
21
|
+
- `kind: 'sections'`: a page is a title and a list of blocks. With `nested: true`
|
|
22
|
+
(the usual `pages` collection at `urlBase: '/'`), the file's path is the address:
|
|
23
|
+
`content/pages/about.json` is `/about`, `content/pages/services/print.json` is
|
|
24
|
+
`/services/print`, and `content/pages/index.json` is Home. An `index.json` anywhere
|
|
25
|
+
deeper is an error: `/services` itself is `content/pages/services.json`.
|
|
26
|
+
- `kind: 'fields'`: each document is a title plus the collection's `fields`. With a
|
|
27
|
+
`urlBase` (`/work/`), `content/<folder>/<slug>.json` is `/work/<slug>`. With no
|
|
28
|
+
`urlBase` the documents have no page of their own (a team, an FAQ); other pages show
|
|
29
|
+
them.
|
|
30
|
+
- **Shared blocks** (one block used on several pages): `content/shared/<name>.json`
|
|
31
|
+
(or the config's `sharedFolder`).
|
|
32
|
+
- **Site Settings**: `content/site-settings.json`, one file.
|
|
33
|
+
- **Schemas**: `schemas/<collection>.schema.json`, `schemas/shared.schema.json`,
|
|
34
|
+
`schemas/site-settings.schema.json`, generated from the config. Every content file's
|
|
35
|
+
first key is `"$schema"`, the relative path to its schema
|
|
36
|
+
(`"../../schemas/pages.schema.json"`). Never edit `schemas/` by hand; `caracms schemas`
|
|
37
|
+
writes it.
|
|
38
|
+
|
|
39
|
+
File names are lower case words with hyphens (`about-us.json`), since they're addresses.
|
|
40
|
+
|
|
41
|
+
## The file shapes
|
|
42
|
+
|
|
43
|
+
A page (`sections`):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"$schema": "../../schemas/pages.schema.json",
|
|
48
|
+
"title": "About",
|
|
49
|
+
"blocks": [
|
|
50
|
+
{ "type": "hero", "heading": ["A small studio", "with big ideas"], "intro": "Six people." },
|
|
51
|
+
{ "type": "text", "heading": "How we work", "body": "One team from start to launch." },
|
|
52
|
+
{ "shared": "start-a-project" }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `title` is required. It names the page in the editor and in the browser's title
|
|
58
|
+
(`<title> · <brand name>`, unless `seo.title` is set; the config's `titleSeparator` can
|
|
59
|
+
make the ` · ` another separator); it isn't necessarily drawn on the page. The home page
|
|
60
|
+
is the exception: its title is its own `seo.title`, else Site Settings' `seo.title`, else
|
|
61
|
+
the brand name (the document's `title` only when all of those are empty). With the
|
|
62
|
+
config's `titleFallback: 'site'`, every page falls back that way, not only the home page.
|
|
63
|
+
- Each block is `{ "type": <a key of the config's blocks>, ...its fields }`. A field left
|
|
64
|
+
out reads as empty.
|
|
65
|
+
- `{ "shared": "<name>" }` places the shared block `content/shared/<name>.json` there.
|
|
66
|
+
- If the collection has `fields` of its own (page fields), they're top-level keys after
|
|
67
|
+
`title` and before `blocks`.
|
|
68
|
+
- An optional `"seo": { "title", "description", "image" }` sets the page's search and
|
|
69
|
+
share text; leave it out to use Site Settings' defaults.
|
|
70
|
+
|
|
71
|
+
A fields document: `{ "$schema", "title", ...fields }`, keys in the config's order.
|
|
72
|
+
|
|
73
|
+
A shared block: `{ "$schema": "../../schemas/shared.schema.json", "title": "Start a project", "block": { "type": "cta", ... } }`.
|
|
74
|
+
|
|
75
|
+
## Field values
|
|
76
|
+
|
|
77
|
+
Match each field's kind in the config (`text()`, `richText()`, …):
|
|
78
|
+
|
|
79
|
+
| Field | Stored as |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `text()` | `"a string"`; with `multiline: true`, `\n` between lines |
|
|
82
|
+
| `lockup({ style })` | lines: `["A small studio", "with big ideas"]`, or with phone lines `{ "lines": [...], "phone": [...] }` |
|
|
83
|
+
| `select({ options })` | one option's `value` |
|
|
84
|
+
| `richText()` | a list, below |
|
|
85
|
+
| `link()` | `{ "label": "Contact us", "href": "/contact" }` |
|
|
86
|
+
| `link({ addressOnly: true })` | `{ "href": "/contact" }` |
|
|
87
|
+
| `buttons({ styles })` | `[{ "label", "href", "style": <a style's value> }]` |
|
|
88
|
+
| `toggle()` | `true` / `false` |
|
|
89
|
+
| `number()` | a number |
|
|
90
|
+
| `date()` | `"2026-10-21"` |
|
|
91
|
+
| `media()` | an image or a clip, below |
|
|
92
|
+
| `reference({ to })` | the referenced document's path in that collection, as a string: `"harbour-coffee"` |
|
|
93
|
+
| `list(field)` | an array of that field's values |
|
|
94
|
+
| `list({ a: …, b: … })` | an array of objects, keys in the config's order |
|
|
95
|
+
| `group({ … })` | one object, keys in the config's order |
|
|
96
|
+
|
|
97
|
+
**Links** (`href`, in links and buttons) are a `/path` on the site, a full `https://` URL,
|
|
98
|
+
`mailto:someone@example.com`, `tel:+353…`, or a fragment: `#anchor`, or `#` alone (a
|
|
99
|
+
placeholder). Anything else shows as plain text.
|
|
100
|
+
|
|
101
|
+
**Rich text** is a list of single-key objects:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"body": [
|
|
105
|
+
{ "p": "We drew the letters from the old **ferry signs**, see [the print work](/services/print)." },
|
|
106
|
+
{ "h2": "From the harbour wall" },
|
|
107
|
+
{ "ul": ["First point", "Second point"] },
|
|
108
|
+
{ "ol": ["Step one", "Step two"] }
|
|
109
|
+
]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `p`, `ul` and `ol` always; `h2` and `h3` only if the field allows them
|
|
113
|
+
(`richText({ headings: ['h2'] })`); never `h1`.
|
|
114
|
+
- Inline: `**bold**`, `*italic*`, `[text](href)` (unless the field has `links: false`),
|
|
115
|
+
`\n` for a line break. Marks open and close inside one paragraph, heading or list item.
|
|
116
|
+
Write a literal `*`, `[`, `]` or `\` as `\*`, `\[`, `\]`, `\\`.
|
|
117
|
+
|
|
118
|
+
**Images** are `{ "key": "uploads/1fb083002e3a/harbour-wall.jpg", "alt": "The harbour wall at dusk", "w": 4000, "h": 2667 }`,
|
|
119
|
+
optionally `"focal": [0.4, 0.3]` (the point to keep in a crop, from the left and the top,
|
|
120
|
+
0–1) or `"decorative": true` (then `"alt": ""`).
|
|
121
|
+
|
|
122
|
+
- The `key` must already be in the site's media library. **You can't upload from a
|
|
123
|
+
content change.** Reuse a key that's already in the content, or ask a person to upload
|
|
124
|
+
the image in the editor (or a developer to run `caracms media import`, which prints the
|
|
125
|
+
key, `w` and `h`).
|
|
126
|
+
- Give every image its `w` and `h`. Without them it still renders, but the page can't keep
|
|
127
|
+
its space while it loads, and `validate` warns. `caracms media import <the image's file>`
|
|
128
|
+
prints them (an image already in the bucket isn't uploaded again). A file's key (a PDF
|
|
129
|
+
in a media slot) needs no size.
|
|
130
|
+
- `alt` says what the image shows, for someone who can't see it.
|
|
131
|
+
- A clip (a Mux video) is `{ "video": "<playback ID>", "play": "loop" | "player" }`; only
|
|
132
|
+
the editor makes those.
|
|
133
|
+
|
|
134
|
+
## Site Settings
|
|
135
|
+
|
|
136
|
+
`content/site-settings.json` holds:
|
|
137
|
+
|
|
138
|
+
- `editors`: the email addresses that can sign in to the editor. **To add a person,** add
|
|
139
|
+
their address to this list.
|
|
140
|
+
- `brand`: `name` (the site's name in the editor and in titles) and `accent` (`#rrggbb`).
|
|
141
|
+
The logo and icon are uploaded in the editor.
|
|
142
|
+
- `seo` (the default search title, description and share image), `googleTag`
|
|
143
|
+
(`G-…` or `GTM-…`), `formRecipients` (`{ "<form id>": ["someone@example.com"] }`),
|
|
144
|
+
`redirects` (`[{ "from": "/old", "to": "/new" }]`).
|
|
145
|
+
- The site's own fields, from `caracms.config.ts` → `settings.fields` (a menu, footer
|
|
146
|
+
links, contact details…).
|
|
147
|
+
|
|
148
|
+
## Moving or deleting a page
|
|
149
|
+
|
|
150
|
+
Prefer the editor for these: it updates links to the page and adds a redirect. By hand:
|
|
151
|
+
rename or delete the file, fix every `href` and `reference` that pointed at it (search
|
|
152
|
+
`content/` for the old address and path), and for a page that was public add
|
|
153
|
+
`{ "from": "/old-address", "to": "/new-address" }` to Site Settings' `redirects`.
|
|
154
|
+
|
|
155
|
+
A page can't go at an address the site's own code serves (a route in `src/pages/`), or at
|
|
156
|
+
`/admin`, `/_caracms` or `/files`.
|
|
157
|
+
|
|
158
|
+
## Choosing blocks for a page
|
|
159
|
+
|
|
160
|
+
**Read the page component before choosing** (the config's `page`, usually
|
|
161
|
+
`src/components/caracms/Page.astro`). How a page looks around its blocks can depend on
|
|
162
|
+
what it's made of: the header's colour or variant, `<main>`'s classes or an intro can
|
|
163
|
+
follow from the page's first block (a map keyed by block type), or from a page field (a
|
|
164
|
+
key after `title`). Pick the first block and the page fields for the look the page should
|
|
165
|
+
have, and compare with an existing page that looks that way.
|
|
166
|
+
|
|
167
|
+
**When no block fits the content,** a content change can't make one: blocks are code, and
|
|
168
|
+
code is the `caracms-blocks` skill's. Don't bend a block that doesn't fit (a heading
|
|
169
|
+
squeezed into a plain text field), and don't write a component as part of a content
|
|
170
|
+
change. Either:
|
|
171
|
+
|
|
172
|
+
- say in the pull request what's missing and what you used instead, or
|
|
173
|
+
- make the block first, as a separate change with the `caracms-blocks` skill, then the
|
|
174
|
+
content on top of it.
|
|
175
|
+
|
|
176
|
+
Either way, tell the person who asked: they may want the block, or a different page.
|
|
177
|
+
|
|
178
|
+
## Getting it merged
|
|
179
|
+
|
|
180
|
+
1. **Work on a new branch** off the site's main branch, and open a pull request into it.
|
|
181
|
+
Never commit to `main` directly.
|
|
182
|
+
2. **Never touch the `drafts` branch.** It's the editor's: it holds editors' unpublished
|
|
183
|
+
changes, and publishing from the editor commits to `main`. Don't push to it, merge it
|
|
184
|
+
or base work on it.
|
|
185
|
+
3. **Run `npx caracms validate` from the site's root before you open the pull request**
|
|
186
|
+
(with pnpm, `pnpm caracms validate`). It checks every content file against
|
|
187
|
+
`caracms.config.ts`:
|
|
188
|
+
- Each problem is a line `file › path.in.the.json: what's wrong`, under `Errors:` or
|
|
189
|
+
`Warnings:`. It exits 1 when there are errors. Fix every error.
|
|
190
|
+
- Warnings (a missing `$schema`, schema files out of date, a heading too long for its
|
|
191
|
+
box, an unknown select value, an image with no `w` or `h`, a file the editor's save
|
|
192
|
+
would reformat) don't break anything, but fix the ones you caused.
|
|
193
|
+
- `npx caracms validate --fix` writes the schemas, adds any missing `$schema` keys and
|
|
194
|
+
lays out each file as the editor saves it, changing only whitespace (that's all it
|
|
195
|
+
fixes). `--json` prints `{ "errors": [...], "warnings": [...] }`,
|
|
196
|
+
each `{ "file", "path", "message" }`.
|
|
197
|
+
4. Keep the pull request to content (`content/`, and `schemas/` if `--fix` changed it).
|
|
198
|
+
Changing blocks or the config is the `caracms-blocks` skill's, in a change of its own
|
|
199
|
+
(see Choosing blocks for a page).
|
|
200
|
+
|
|
201
|
+
The site builds from these files, so a merged content change is live on the next deploy.
|