@scribe-platform/cli 0.2.2 → 0.5.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/README.md +74 -4
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/schema.js +51 -0
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/submission.d.ts.map +1 -1
- package/dist/commands/submission.js +74 -22
- package/dist/commands/submission.js.map +1 -1
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +93 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +62 -11
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +66 -9
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/skill.d.ts +45 -0
- package/dist/lib/skill.d.ts.map +1 -0
- package/dist/lib/skill.js +70 -0
- package/dist/lib/skill.js.map +1 -0
- package/package.json +3 -1
- package/skills/scribe-site-builder/SKILL.md +373 -0
- package/skills/scribe-site-builder/recipes/README.md +31 -0
- package/skills/scribe-site-builder/recipes/collections.md +87 -0
- package/skills/scribe-site-builder/recipes/deploy.md +122 -0
- package/skills/scribe-site-builder/recipes/forms.md +231 -0
- package/skills/scribe-site-builder/recipes/schema.md +99 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scribe-site-builder
|
|
3
|
+
description: Guides building a website on Scribe CMS end to end — designing the Astro site freely against Scribe as its backend, editing the `.scribe.yml` schema, adding one or many forms, the local dev loop, pushing content, and deploying — using the `scribe` CLI and the recipes in `recipes/`. Also the reference for working an existing Scribe site (adding fields/collections, publishing content, triaging form submissions, deleting a site). Use whenever the user wants to build/create/start a new website or site with Scribe, add a form to a site, work on an existing Scribe site, or asks about the `scribe` CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building a site with Scribe
|
|
7
|
+
|
|
8
|
+
Source of truth: this file is the merged, agent-facing version of
|
|
9
|
+
`docs/BUILDER-GUIDE.md` and `docs/AGENT-GUIDE.md` in the
|
|
10
|
+
`ai-xerro-agent/projects/scribe` repo. If those docs and this one ever
|
|
11
|
+
disagree, the repo docs win — this file may be behind.
|
|
12
|
+
|
|
13
|
+
## Recipes — read the one you need
|
|
14
|
+
|
|
15
|
+
Per RFC 0010 the Astro site is **yours to design**. Scribe doesn't supply a
|
|
16
|
+
template you must conform to; it supplies patterns. Load the relevant file
|
|
17
|
+
instead of reading all of them:
|
|
18
|
+
|
|
19
|
+
| Recipe | Use it when |
|
|
20
|
+
|---|---|
|
|
21
|
+
| [`recipes/schema.md`](./recipes/schema.md) | you need the Site's collections and field types |
|
|
22
|
+
| [`recipes/collections.md`](./recipes/collections.md) | rendering editor-managed content |
|
|
23
|
+
| [`recipes/forms.md`](./recipes/forms.md) | adding one or more forms |
|
|
24
|
+
| [`recipes/deploy.md`](./recipes/deploy.md) | build/deploy setup or debugging |
|
|
25
|
+
|
|
26
|
+
[`recipes/README.md`](./recipes/README.md) lists the five things that are a hard
|
|
27
|
+
contract (because Scribe writes into the repo) as opposed to advice.
|
|
28
|
+
|
|
29
|
+
## Mental model
|
|
30
|
+
|
|
31
|
+
- **A site = a GitHub repo.** Schema, content, and the Astro source all live in one
|
|
32
|
+
repo. The admin app and the `scribe` CLI are both just clients that read/write
|
|
33
|
+
files in it via the GitHub API; a local `git clone` is a third client reading/writing
|
|
34
|
+
the same files via git.
|
|
35
|
+
- **Form submissions are the exception — they live in MongoDB, not the repo** (RFC
|
|
36
|
+
0004). A Visitor's name, email and message used to be committed as a YAML file into
|
|
37
|
+
the site's public repo, which published them to the open internet. Nothing writes
|
|
38
|
+
submissions to a repo any more: a submission is a Mongo document with a status and
|
|
39
|
+
an append-only event history, and `content/<form-collection>/` doesn't exist.
|
|
40
|
+
- **`.scribe.yml`** at the repo root is the schema — every collection (content type)
|
|
41
|
+
and its fields, plus which collections are forms. Prefer the CLI over hand-editing,
|
|
42
|
+
because it validates before committing and because the admin treats the committed
|
|
43
|
+
file as authoritative: `scribe schema show <site>` to read,
|
|
44
|
+
`scribe schema apply <site> --file <spec>` to create or replace a whole collection,
|
|
45
|
+
`scribe schema add`/`remove` for a bare stub or a removal. Hand-editing still works
|
|
46
|
+
for a one-line tweak in a clone.
|
|
47
|
+
- **A Site can have many forms.** Each `form: true` collection gets its own inbox,
|
|
48
|
+
its own workflow states and its own nav badge in the admin, and posts to
|
|
49
|
+
`/api/submissions?site=…&collection=…`. See `recipes/forms.md`.
|
|
50
|
+
- **No merge tooling.** Admin-UI edits and local edits both land as commits on the
|
|
51
|
+
same repo. If they touch the same file, resolve it with plain `git pull` / merge —
|
|
52
|
+
there's no `scribe` conflict command.
|
|
53
|
+
- **`scripts/generate-config.mjs` is legacy.** Sites scaffolded before RFC 0010 have
|
|
54
|
+
it, and it regenerates `src/content.config.ts` and `src/site-config.ts` on
|
|
55
|
+
`predev`/`prebuild` — so in those repos, don't hand-edit those two files, it will
|
|
56
|
+
overwrite you. It flattens the schema lossily (field *names* with no types, and
|
|
57
|
+
only the **first** `form: true` collection), so a site that needs real types or a
|
|
58
|
+
second form should read `.scribe.yml` directly instead — see `recipes/schema.md`.
|
|
59
|
+
New sites shouldn't add it.
|
|
60
|
+
|
|
61
|
+
## Setup (once)
|
|
62
|
+
|
|
63
|
+
- **CLI**: confirm `scribe --help` works. If it's not found, `npm link` from
|
|
64
|
+
`packages/cli` in the `ai-xerro-agent/projects/scribe` repo.
|
|
65
|
+
- **Login**: `scribe login --token <key>` — mint the key from the
|
|
66
|
+
admin UI (sign in → account menu → **API Tokens** → New token). Defaults to the
|
|
67
|
+
hosted admin at `https://scribe.weaverdigital.io`; add `--api-url <url>` only to
|
|
68
|
+
point at a different instance (a local dev/Docker admin). Writes
|
|
69
|
+
`~/.scribe/config.json`; every other command reads from there, no per-invocation
|
|
70
|
+
`--token` flag. If a command fails with a raw `fetch failed`, that's not an API
|
|
71
|
+
error — it means `apiUrl` in that config doesn't point at anything listening;
|
|
72
|
+
re-run `scribe login` with the right URL.
|
|
73
|
+
|
|
74
|
+
## Keeping Scribe up to date
|
|
75
|
+
|
|
76
|
+
`scribe update` updates the CLI **and** reinstalls this skill, which ships inside
|
|
77
|
+
the `@scribe-platform/cli` npm package:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
scribe update
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Two things to know:
|
|
84
|
+
|
|
85
|
+
- **Tell the user to restart Claude Code afterwards.** Claude Code only discovers
|
|
86
|
+
skills at session start, so the session that ran the update is still reading the
|
|
87
|
+
old copy of this file. If you need the new instructions in the current session,
|
|
88
|
+
read `~/.claude/skills/scribe-site-builder/SKILL.md` from disk instead of waiting
|
|
89
|
+
for a restart.
|
|
90
|
+
- **It only works for a global npm install** (`npm install -g
|
|
91
|
+
@scribe-platform/cli`). In a dev checkout of the scribe repo it refuses, and if
|
|
92
|
+
`~/.claude/skills/scribe-site-builder` is a symlink into a checkout it leaves it
|
|
93
|
+
alone — that's an author editing the skill in place.
|
|
94
|
+
|
|
95
|
+
Run it when the user says there's a new version, or when a CLI command fails in a
|
|
96
|
+
way that suggests this skill is behind the API (an option the CLI doesn't
|
|
97
|
+
recognise, a command that no longer exists).
|
|
98
|
+
|
|
99
|
+
## Workflow: building a new site
|
|
100
|
+
|
|
101
|
+
1. **Scaffold.**
|
|
102
|
+
```
|
|
103
|
+
scribe site add --slug my-site
|
|
104
|
+
```
|
|
105
|
+
Creates the GitHub repo, pushes the Astro starter template
|
|
106
|
+
(home/blog/contact pages, a default `.scribe.yml` with `posts` and `inquiries`
|
|
107
|
+
collections), and registers the site with the admin — one call.
|
|
108
|
+
`--name` sets a display name (defaults to the slug); `--form-collection` renames
|
|
109
|
+
the default contact-form collection (defaults to `inquiries`).
|
|
110
|
+
|
|
111
|
+
`--domain` and `--turnstile-key` are both optional and normally omitted. The
|
|
112
|
+
domain defaults to the `scribe-platform.github.io` Pages host every new site is
|
|
113
|
+
served from, and the Turnstile site key defaults to the shared widget configured
|
|
114
|
+
on the admin app — one widget covers all sites, scoped by its allowed-domain
|
|
115
|
+
list rather than by key. Pass them only to override those defaults. The real
|
|
116
|
+
customer domain comes later, at step 8.
|
|
117
|
+
|
|
118
|
+
2. **Clone it locally.**
|
|
119
|
+
```
|
|
120
|
+
scribe site clone my-site
|
|
121
|
+
```
|
|
122
|
+
Plain `git clone`. Nothing Scribe-specific happens locally — `npm run dev` works,
|
|
123
|
+
`.scribe.yml` is just a file in the repo root.
|
|
124
|
+
|
|
125
|
+
3. **Define/edit the schema.** Hand-edit `.scribe.yml` — add fields under
|
|
126
|
+
`collections[].fields[]` (see field reference below). Once saved,
|
|
127
|
+
`npm run dev` / `npm run build` locally regenerate the Astro content types
|
|
128
|
+
automatically (via `predev`/`prebuild`) — no separate sync step. A `git push` of
|
|
129
|
+
`.scribe.yml` is enough to reach the admin, which reads it live from GitHub,
|
|
130
|
+
cached **8 minutes** per repo — after a schema push, the admin UI may lag up to
|
|
131
|
+
8 minutes before showing it.
|
|
132
|
+
|
|
133
|
+
4. **Build content locally.** Normal Astro dev loop — content files live under
|
|
134
|
+
`content/<collection>/*` per the collection's `path`/`format`.
|
|
135
|
+
|
|
136
|
+
5. **Push to Scribe.** Two ways:
|
|
137
|
+
- `git push` — commits schema and/or content straight to the repo. Normal path.
|
|
138
|
+
- `scribe content publish <site> <file>` — pushes one local file through the
|
|
139
|
+
API, validating it against the schema server-side first (a raw `git push`
|
|
140
|
+
skips that validation).
|
|
141
|
+
|
|
142
|
+
6. **Admin-UI edits, pulled back down.** The admin UI writes commits to the same
|
|
143
|
+
repo — `git pull` locally to pick them up. Conflicts resolve as ordinary git
|
|
144
|
+
merge conflicts, nothing purpose-built.
|
|
145
|
+
|
|
146
|
+
7. **Deploy.** Any push to the repo's default branch runs `.github/workflows/deploy.yml`
|
|
147
|
+
automatically (build + GitHub Pages). `scribe build trigger my-site` dispatches
|
|
148
|
+
the same workflow on demand without a new commit — debounced to once per 5
|
|
149
|
+
minutes per site, so repeat calls in that window are a silent no-op.
|
|
150
|
+
|
|
151
|
+
8. **Domain cutover — only when a customer's own hostname is ready.** Skip this
|
|
152
|
+
entirely for demo sites; the GitHub Pages default already works.
|
|
153
|
+
|
|
154
|
+
Check what DNS the domain needs first. This changes nothing:
|
|
155
|
+
```
|
|
156
|
+
scribe site promote my-site --domain example.com --dns-only
|
|
157
|
+
```
|
|
158
|
+
It prints the records to add at the registrar. An apex domain needs the four A
|
|
159
|
+
records; a subdomain needs only the CNAME. **In Cloudflare the record must be
|
|
160
|
+
DNS-only (grey cloud), not proxied** — the orange-cloud proxy blocks the
|
|
161
|
+
challenge GitHub uses to issue the site's certificate.
|
|
162
|
+
|
|
163
|
+
Once the records are in place and resolving:
|
|
164
|
+
```
|
|
165
|
+
scribe site promote my-site --domain example.com
|
|
166
|
+
```
|
|
167
|
+
This does the whole cutover in one go: writes `public/CNAME`, registers the
|
|
168
|
+
domain with GitHub Pages, points the contact form at the admin app, updates the
|
|
169
|
+
hostname the public form endpoint accepts submissions from, adds the domain to
|
|
170
|
+
the Turnstile widget, enforces HTTPS, and redeploys.
|
|
171
|
+
|
|
172
|
+
It **refuses to run until DNS actually points at GitHub Pages**, because the
|
|
173
|
+
cutover kills the old project-page URL — if DNS isn't ready the site is
|
|
174
|
+
unreachable at both addresses. Fix the DNS and run it again rather than reaching
|
|
175
|
+
for `--force`.
|
|
176
|
+
|
|
177
|
+
**Re-running is safe and is the fix for anything that goes wrong.** Every step
|
|
178
|
+
checks the current state before writing, so a run that fails halfway is repaired
|
|
179
|
+
by running the same command again. If it reports that GitHub is still issuing
|
|
180
|
+
the certificate, wait a few minutes and re-run to finish — until then `http://`
|
|
181
|
+
won't serve the site.
|
|
182
|
+
|
|
183
|
+
If it prints a `Warning:` about the Turnstile widget, the domain list did **not**
|
|
184
|
+
sync — fix it in the Cloudflare dashboard, or the contact form silently rejects
|
|
185
|
+
real visitors on that domain.
|
|
186
|
+
|
|
187
|
+
`scribe site update my-site --domain example.com` only edits the stored record
|
|
188
|
+
and the Turnstile widget. Use it to correct a domain, never to cut a site over.
|
|
189
|
+
|
|
190
|
+
## Working an existing site
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
scribe content list <site> [--collection <name>] [--json]
|
|
194
|
+
scribe content read <site> <path> [--json]
|
|
195
|
+
scribe content publish <site> <file> # validates against schema
|
|
196
|
+
scribe content delete <site> <path>
|
|
197
|
+
scribe content versions <site> <path> [--json] # commit history
|
|
198
|
+
scribe content revert <site> <path> --sha <sha> # checks out old version as new commit
|
|
199
|
+
|
|
200
|
+
scribe schema show <site> [collection] [--json] # read the schema
|
|
201
|
+
scribe schema apply <site> --file <spec|-> # create/replace a whole collection
|
|
202
|
+
scribe schema add <site> <collection-name> [--path <path>] # bare stub only, no fields
|
|
203
|
+
scribe schema remove <site> <collection-name> # deletes its folder too
|
|
204
|
+
|
|
205
|
+
scribe submission list <site> <collection> [--status <status>] [--page <n>] [--json]
|
|
206
|
+
scribe submission update <site> <collection> <id> [--status <s>] [--note <n>]
|
|
207
|
+
scribe submission export <site> <collection> [--format csv|json] [--status <s>] [--out <file>]
|
|
208
|
+
scribe submission delete <site> <collection> <id> [--yes] # staff only
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- `scribe schema apply` is the way to add a form (or any collection with fields): it
|
|
212
|
+
takes a YAML/JSON file describing **one whole collection**, upserts it by `name`,
|
|
213
|
+
and validates through the admin's own parser first — so a reserved `status` field,
|
|
214
|
+
an `image` field on a form collection, or a malformed workflow is reported before
|
|
215
|
+
anything is committed. `--file -` reads from a pipe. Full worked example in
|
|
216
|
+
[`recipes/forms.md`](./recipes/forms.md).
|
|
217
|
+
- `scribe schema add` only appends a bare `{name, path}` stub with no fields; it's for
|
|
218
|
+
starting a content collection interactively. There is still no *field*-level
|
|
219
|
+
command — `schema apply` replaces the whole collection, so include every field.
|
|
220
|
+
- `scribe content publish` reads a **local file at the given repo-relative path**,
|
|
221
|
+
parsing `---`-delimited YAML/JSON frontmatter (or a bare YAML/JSON object with no
|
|
222
|
+
body).
|
|
223
|
+
- `scribe submission list` paginates (1-based `--page`); `--json` gives the whole page
|
|
224
|
+
object, so `total` tells you whether there's a page 2.
|
|
225
|
+
- **`<id>` is a Mongo ObjectId**, as printed by `submission list` — not a filename and
|
|
226
|
+
not a repo path. There's no path inference any more; submissions aren't files.
|
|
227
|
+
- `--note` **appends** to the submission's history. It never overwrites an earlier
|
|
228
|
+
note, and it leaves the submission's own `updatedAt` alone — a note is something
|
|
229
|
+
someone wrote *about* the lead, not a change to it.
|
|
230
|
+
- `submission export` writes to stdout unless `--out` is given, so it pipes
|
|
231
|
+
(`scribe submission export my-site inquiries > leads.csv`). CSV is for a
|
|
232
|
+
spreadsheet and is lossy on purpose: its columns come from the collection's schema,
|
|
233
|
+
so the header row is identical between exports even when a batch left a field blank,
|
|
234
|
+
and it carries no history and no visitor IP or user agent. `--format json` is
|
|
235
|
+
lossless, with each submission's events nested under it.
|
|
236
|
+
- `submission delete` is permanent, cascades the submission's history, and needs a
|
|
237
|
+
**staff** role — a site user's API key gets a 403. It prompts for the id unless
|
|
238
|
+
`--yes` is passed. There is deliberately no equivalent in the admin UI, so don't go
|
|
239
|
+
looking for a delete button on the inbox.
|
|
240
|
+
|
|
241
|
+
## Images — never hotlink, always commit the file
|
|
242
|
+
|
|
243
|
+
**A `type: image` field is just a string.** Nothing in Scribe validates that its
|
|
244
|
+
value is an uploaded image — the schema doesn't know or care whether you write
|
|
245
|
+
`/uploads/some-photo.jpg` or `https://images.unsplash.com/photo-xyz`. When you (the
|
|
246
|
+
agent) are hand-authoring content files directly in a cloned repo — which is the
|
|
247
|
+
normal CLI workflow — **do not put an external image URL (Unsplash or otherwise)
|
|
248
|
+
into an image field's value.** It looks fine at first glance and then quietly means
|
|
249
|
+
the site depends forever on a third-party host it doesn't control, the image was
|
|
250
|
+
never actually reviewed/chosen for that specific listing, and it breaks the "this
|
|
251
|
+
site's content lives in this repo" model the rest of Scribe assumes.
|
|
252
|
+
|
|
253
|
+
Instead, every image an agent puts into content must be a **real file committed to
|
|
254
|
+
the site's own repo**, the same way the admin's own upload feature does it:
|
|
255
|
+
|
|
256
|
+
1. Get the actual image bytes — download it (a real stock photo, something the
|
|
257
|
+
user supplied, whatever the source) to a local temp file.
|
|
258
|
+
2. Save it into the cloned repo at `public/uploads/<name>`, with a name unique
|
|
259
|
+
enough not to collide (mirror the admin's own convention:
|
|
260
|
+
`<timestamp>-<safe-filename>`, letters/digits/`._-` only).
|
|
261
|
+
3. Reference it in the content file's frontmatter as `/uploads/<name>` — a
|
|
262
|
+
repo-relative path, not the original external URL.
|
|
263
|
+
4. Commit and push (`public/uploads/...` + the content file) together, same as any
|
|
264
|
+
other content change. There's no `scribe` CLI command for this — it's a plain
|
|
265
|
+
file in the repo, same as everything else.
|
|
266
|
+
|
|
267
|
+
This applies to `list: true` image fields too — one committed file per array entry,
|
|
268
|
+
same convention. A single-image field (`type: image`, no `list`) and a list one both
|
|
269
|
+
end up as `/uploads/...` paths; only the shape (string vs. array of strings)
|
|
270
|
+
differs.
|
|
271
|
+
|
|
272
|
+
The one case where a bare external URL is fine: the user explicitly says to leave a
|
|
273
|
+
placeholder / link out to somewhere else on purpose. Don't default to that — treat
|
|
274
|
+
"add a photo of X" as "go get a real image and commit it," not "find any URL that
|
|
275
|
+
renders."
|
|
276
|
+
|
|
277
|
+
## Deleting a site
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
scribe site delete <slug> [--yes]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Permanent — deletes the GitHub repo (schema, content, Astro source) and every Mongo
|
|
284
|
+
record for the site, which since RFC 0004 includes its submissions and their event
|
|
285
|
+
history. No undo. Prompts to retype the
|
|
286
|
+
slug unless `--yes` is passed. Requires an **unrestricted** API key (no
|
|
287
|
+
`siteSlugs` restriction) — same rule as `site add`; a key scoped to only that
|
|
288
|
+
site is rejected. Same action is in the admin UI: account menu → **Settings**
|
|
289
|
+
(super-admin only) → **Danger zone**.
|
|
290
|
+
|
|
291
|
+
Deleting also drops the site's domain from the Turnstile widget — but only if no
|
|
292
|
+
other site still uses that domain, so removing one demo never breaks the contact
|
|
293
|
+
form on the rest. A `Warning:` in the output means that cleanup didn't happen and
|
|
294
|
+
the stale domain is still on the widget; harmless, but tidy it up in Cloudflare.
|
|
295
|
+
|
|
296
|
+
Side effect worth knowing: if an API key's `siteSlugs` was only that one site,
|
|
297
|
+
deleting it leaves that key with an empty `siteSlugs` array — which the API treats
|
|
298
|
+
as **unrestricted** (every site), not inert. A key scoped to exactly the site you
|
|
299
|
+
delete comes out of it more powerful, not revoked.
|
|
300
|
+
|
|
301
|
+
## `.scribe.yml` field reference
|
|
302
|
+
|
|
303
|
+
Per collection: `name`, `label`, `path`, `format` (`yaml-frontmatter` for normal
|
|
304
|
+
content; also
|
|
305
|
+
`json-frontmatter`/`toml-frontmatter`/`yaml`/`json`/`toml`/`datagrid`/`code`/`raw`),
|
|
306
|
+
`fields[]`, `view` (`{primary, sort, fields, search, default}` — controls the admin
|
|
307
|
+
list view), and the Scribe extensions `description` (shown under the title on the
|
|
308
|
+
collection's list/inbox page in the admin), `form: true` (renders as an inbox),
|
|
309
|
+
`workflow: {states}` (form collections only), `notify: [...]` (recipients on new
|
|
310
|
+
submissions).
|
|
311
|
+
|
|
312
|
+
On a **form collection** specifically, since RFC 0004 moved submissions into Mongo:
|
|
313
|
+
|
|
314
|
+
- **Many per site is supported and normal.** Every `form: true` collection is an
|
|
315
|
+
independent inbox with its own workflow, badge and `?collection=` value; a schema
|
|
316
|
+
with two of them needs no special handling. The rate limiter is the one shared
|
|
317
|
+
thing — it's keyed per Site and IP, not per form.
|
|
318
|
+
- **Submitted values are coerced to the declared type** before validation, so a
|
|
319
|
+
form posting `FormData` works: a checkbox's `"on"` becomes `true`, `"4"` becomes
|
|
320
|
+
`4`. A value that can't be coerced still fails with a readable message. JSON
|
|
321
|
+
bodies, url-encoded and multipart are all accepted.
|
|
322
|
+
- `path` and `format` are **ignored** — there are no files to lay out. Leave them;
|
|
323
|
+
they're harmless, and the parser doesn't read them for a form collection.
|
|
324
|
+
- `workflow` takes `{states: [...]}` only. The old `field:` key is gone, and so is the
|
|
325
|
+
legacy shorthand where `states` was a list of bare strings — a schema still written
|
|
326
|
+
that way now fails to parse rather than being silently guessed at. Each state is an
|
|
327
|
+
object: `{name, label, kind, hidden}`, where `kind` is `needs-you` | `waiting` |
|
|
328
|
+
`closed` (it drives the inbox's nav badge, which counts `needs-you`) and `hidden`
|
|
329
|
+
keeps a state like `spam` out of the default view while leaving it selectable.
|
|
330
|
+
- **`status` and `createdAt` are reserved field names** and can't be declared in
|
|
331
|
+
`fields[]`. `status` is a real column on the submission and `submittedAt` replaces
|
|
332
|
+
`createdAt`; both render as columns the schema doesn't have to mention.
|
|
333
|
+
- **An `image` field is rejected** on a form collection. The old path uploaded a
|
|
334
|
+
Visitor's file straight into the public repo, and a Mongo document is the wrong place
|
|
335
|
+
for a blob. Deferred to RFC 0007 (object storage).
|
|
336
|
+
- **`notify:` does not send email.** It runs, but through a `ConsoleMailer` that only
|
|
337
|
+
logs — no address has ever received anything. Real delivery is RFC 0009. Don't
|
|
338
|
+
promise a client they'll be emailed on a new submission; the inbox and its
|
|
339
|
+
`needs-you` badge are the actual notification today.
|
|
340
|
+
|
|
341
|
+
Per field: `name`, `type`, `label`, `required`, `default`, `pattern`, `hidden`,
|
|
342
|
+
`readonly`, `isBody` (this field is the markdown body, not frontmatter), `list`
|
|
343
|
+
(`true` or `{min, max, collapsible}`), `options` (for `type: select`). For
|
|
344
|
+
`type: image` fields specifically, see **Images** above — its value (or each
|
|
345
|
+
value in the array, if `list`) must be a committed `/uploads/...` path, not an
|
|
346
|
+
external URL.
|
|
347
|
+
|
|
348
|
+
**Field `type` — stick to the implemented list.** The admin has a working editor
|
|
349
|
+
for: `string`, `text`, `rich-text`, `number`, `boolean`, `date`, `image`, `select`.
|
|
350
|
+
The type enum also accepts `block`, `code`, `file`, `object`, `reference`, `uuid` —
|
|
351
|
+
these parse fine but the admin has no editor for them yet, so a field using one
|
|
352
|
+
falls back to a raw/unstyled view. Don't reach for them unless that's specifically
|
|
353
|
+
acceptable for this site.
|
|
354
|
+
|
|
355
|
+
**Quote date- and time-like values in `string` fields.** When hand-writing a
|
|
356
|
+
content file, `when: 2026-09-28` or `at: 12:30` is read by Astro's YAML loader as
|
|
357
|
+
a Date and a number, not a string — so a field declared `type: string` fails its
|
|
358
|
+
generated `z.string()` schema and **the site build breaks**. Write
|
|
359
|
+
`when: "2026-09-28"`. Fields declared `type: date` are immune (they generate
|
|
360
|
+
`z.coerce.date()`, which takes either form), and anything written by the admin or
|
|
361
|
+
by `scribe content publish` is quoted for you. This only bites on raw hand-edited
|
|
362
|
+
files committed with `git push`.
|
|
363
|
+
|
|
364
|
+
## Gotchas
|
|
365
|
+
|
|
366
|
+
- Schema changes are instant locally, but cached **8 minutes** in the admin/API
|
|
367
|
+
after a push — don't chase "why hasn't this shown up yet" faster than that.
|
|
368
|
+
- `scribe site clone` is a thin `git clone` wrapper — once cloned, it's an ordinary
|
|
369
|
+
git repo, nothing more.
|
|
370
|
+
- A raw `git push` of content skips schema validation; `scribe content publish`
|
|
371
|
+
doesn't.
|
|
372
|
+
- There's no merge-conflict tooling anywhere in the CLI — resolve like any git
|
|
373
|
+
conflict.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Patterns for building an Astro site against Scribe as the backend. Per
|
|
4
|
+
[RFC 0010](../../../docs/proposals/0010-agent-authored-forms.md) these are
|
|
5
|
+
**advice, not enforcement** — read the one you need, adapt it to the site you're
|
|
6
|
+
building, and ignore the rest. There is no template to conform to and no
|
|
7
|
+
generated config to keep in sync.
|
|
8
|
+
|
|
9
|
+
| Recipe | Use it when |
|
|
10
|
+
|---|---|
|
|
11
|
+
| [`schema.md`](./schema.md) | you need to know what collections and fields a Site has |
|
|
12
|
+
| [`collections.md`](./collections.md) | rendering content (posts, products, events) that editors manage in Scribe |
|
|
13
|
+
| [`forms.md`](./forms.md) | putting one or more forms on the Site |
|
|
14
|
+
| [`deploy.md`](./deploy.md) | setting up or debugging the build and deploy |
|
|
15
|
+
|
|
16
|
+
## The part that isn't advice
|
|
17
|
+
|
|
18
|
+
Scribe doesn't only serve the Site — it **writes into the Site's repo** and
|
|
19
|
+
triggers its build. The admin commits content files and uploaded images, writes
|
|
20
|
+
`public/CNAME` when a domain is verified, and `scribe build trigger` dispatches
|
|
21
|
+
the deploy workflow. So these five things are a contract, not a preference:
|
|
22
|
+
|
|
23
|
+
1. Content lives at each collection's `path`, in its `format`.
|
|
24
|
+
2. Images live under `public/` at the path stored in the item's frontmatter.
|
|
25
|
+
3. The presence of `public/CNAME` decides Astro's `base`.
|
|
26
|
+
4. The deploy workflow accepts `workflow_dispatch`.
|
|
27
|
+
5. Forms POST to `/api/submissions?site=<slug>&collection=<name>`.
|
|
28
|
+
|
|
29
|
+
Get one of these wrong and the failure shows up *after* an editor hits publish,
|
|
30
|
+
on the live site, not while you're building. Everything else — routing, styling,
|
|
31
|
+
page structure, which pages exist, where a form goes — is yours.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Recipe: rendering content collections
|
|
2
|
+
|
|
3
|
+
A content collection (anything without `form: true`) is a folder of files the
|
|
4
|
+
admin writes directly into the repo. Your job is to read them from the `path` the
|
|
5
|
+
schema declares, in the `format` it declares. That part isn't negotiable — the
|
|
6
|
+
admin commits those files and expects them there. How you route, style and lay
|
|
7
|
+
them out is entirely yours.
|
|
8
|
+
|
|
9
|
+
## The two formats
|
|
10
|
+
|
|
11
|
+
- **`yaml-frontmatter`** — markdown: `---` frontmatter, then a body. The body is
|
|
12
|
+
whichever field has `isBody: true`.
|
|
13
|
+
- **`yaml`** — a plain `.yml` data file, no body.
|
|
14
|
+
|
|
15
|
+
Files are named `<slug>.md` / `<slug>.yml`, and the slug is the URL slug.
|
|
16
|
+
|
|
17
|
+
## With Astro content collections
|
|
18
|
+
|
|
19
|
+
Glob the schema's paths. Astro wants its own `src/content.config.ts`, so keep the
|
|
20
|
+
loader list in step with `.scribe.yml` rather than hardcoding one Site's names:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// src/content.config.ts
|
|
24
|
+
import { defineCollection, z } from 'astro:content';
|
|
25
|
+
import { glob } from 'astro/loaders';
|
|
26
|
+
|
|
27
|
+
const cars = defineCollection({
|
|
28
|
+
loader: glob({ pattern: '**/*.md', base: './content/cars' }),
|
|
29
|
+
// Loose on purpose: an editor adding a field in Scribe shouldn't fail the
|
|
30
|
+
// build before you've had a chance to render it.
|
|
31
|
+
schema: z.object({
|
|
32
|
+
title: z.string(),
|
|
33
|
+
make: z.string().optional(),
|
|
34
|
+
year: z.number().optional(),
|
|
35
|
+
price: z.number().optional(),
|
|
36
|
+
images: z.array(z.string()).default([]),
|
|
37
|
+
active: z.boolean().default(true),
|
|
38
|
+
}).passthrough(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const collections = { cars };
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then render as usual with `getCollection('cars')`.
|
|
45
|
+
|
|
46
|
+
Keep the zod schema **permissive** (`.passthrough()`, `.optional()`). A strict
|
|
47
|
+
schema turns "an editor added a field" into a failed deploy, and the editor has
|
|
48
|
+
no way to see why.
|
|
49
|
+
|
|
50
|
+
## Images
|
|
51
|
+
|
|
52
|
+
Uploaded images are committed under `public/` and the frontmatter stores the
|
|
53
|
+
**absolute site path** — `/images/cars/bugatti-chiron.jpg`. Two consequences:
|
|
54
|
+
|
|
55
|
+
- Don't use `astro:assets` `Image` on them; they aren't in `src/`. A plain `<img
|
|
56
|
+
src={...}>` is correct.
|
|
57
|
+
- On a project page (no custom domain) the Astro `base` is `/<repo>`, so prefix
|
|
58
|
+
stored paths with it or they 404 in the demo:
|
|
59
|
+
|
|
60
|
+
```astro
|
|
61
|
+
---
|
|
62
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
63
|
+
---
|
|
64
|
+
<img src={`${base}${car.data.images[0]}`} alt={car.data.title} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is the single most common way a demo site looks broken while the custom
|
|
68
|
+
domain version looks fine.
|
|
69
|
+
|
|
70
|
+
## Don't hardcode one Client's collections
|
|
71
|
+
|
|
72
|
+
The same code gets reused across Sites whose collections are named differently.
|
|
73
|
+
Drive pages off the schema where it's cheap:
|
|
74
|
+
|
|
75
|
+
```astro
|
|
76
|
+
---
|
|
77
|
+
import { contentCollections } from '../lib/scribe';
|
|
78
|
+
const sections = contentCollections().filter((c) => c.name !== 'posts');
|
|
79
|
+
---
|
|
80
|
+
{sections.map((c) => <a href={`/${c.name}`}>{c.label ?? c.name}</a>)}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Local content
|
|
84
|
+
|
|
85
|
+
`scribe content pull <site>` fetches the live content into the repo so you can
|
|
86
|
+
build against real data. A Site also usually keeps a `content/` folder committed,
|
|
87
|
+
which is what the deploy builds from.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Recipe: build and deploy
|
|
2
|
+
|
|
3
|
+
Sites deploy to GitHub Pages from their own repo. Two parts of this are a
|
|
4
|
+
contract with Scribe rather than a preference: the workflow must accept
|
|
5
|
+
`workflow_dispatch`, and the Astro `base` must follow `public/CNAME`.
|
|
6
|
+
|
|
7
|
+
## Why `workflow_dispatch` matters
|
|
8
|
+
|
|
9
|
+
When an editor publishes in the admin, Scribe commits the content file and then
|
|
10
|
+
calls `scribe build trigger` → `dispatchWorkflow`. Without
|
|
11
|
+
`workflow_dispatch: {}` the dispatch fails and **the editor's change never goes
|
|
12
|
+
live** even though the admin says it saved. Keep it even if you also build on
|
|
13
|
+
push.
|
|
14
|
+
|
|
15
|
+
## Why the base path follows CNAME
|
|
16
|
+
|
|
17
|
+
A bare project page lives at `https://<org>.github.io/<repo>/`, so Astro needs
|
|
18
|
+
`base: '/<repo>'`. Once a custom domain is verified, the admin writes
|
|
19
|
+
`public/CNAME` and the site serves from the root, so `base` must become `/`.
|
|
20
|
+
Deciding this in the workflow means a domain going live needs no code change.
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# .github/workflows/deploy.yml
|
|
24
|
+
name: Deploy to GitHub Pages
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
push:
|
|
28
|
+
branches: [main]
|
|
29
|
+
workflow_dispatch: {} # required — Scribe dispatches this on publish
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
pages: write
|
|
34
|
+
id-token: write
|
|
35
|
+
|
|
36
|
+
concurrency:
|
|
37
|
+
group: pages
|
|
38
|
+
cancel-in-progress: true
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
build:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: 22
|
|
48
|
+
- run: npm install
|
|
49
|
+
|
|
50
|
+
- name: Compute Pages base path
|
|
51
|
+
id: base
|
|
52
|
+
run: |
|
|
53
|
+
if [ -f public/CNAME ]; then
|
|
54
|
+
echo "path=/" >> "$GITHUB_OUTPUT"
|
|
55
|
+
else
|
|
56
|
+
echo "path=/${{ github.event.repository.name }}" >> "$GITHUB_OUTPUT"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
- run: npm run build
|
|
60
|
+
env:
|
|
61
|
+
PUBLIC_SCRIBE_API_URL: ${{ vars.PUBLIC_SCRIBE_API_URL }}
|
|
62
|
+
PUBLIC_BASE_PATH: ${{ steps.base.outputs.path }}
|
|
63
|
+
|
|
64
|
+
- uses: actions/upload-pages-artifact@v3
|
|
65
|
+
with:
|
|
66
|
+
path: dist
|
|
67
|
+
|
|
68
|
+
deploy:
|
|
69
|
+
needs: build
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
environment:
|
|
72
|
+
name: github-pages
|
|
73
|
+
steps:
|
|
74
|
+
- id: deployment
|
|
75
|
+
uses: actions/deploy-pages@v4
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
And in `astro.config.mjs`:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
base: process.env.PUBLIC_BASE_PATH || '/',
|
|
83
|
+
// ...
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## `PUBLIC_SCRIBE_API_URL`
|
|
88
|
+
|
|
89
|
+
Where forms post. It's a **repo variable**, and GitHub resolves an unset
|
|
90
|
+
`vars.*` to an empty string rather than leaving it undefined — so `??` won't
|
|
91
|
+
fall back. Use `||`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const apiBase = import.meta.env.PUBLIC_SCRIBE_API_URL || 'https://scribe.weaverdigital.io';
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Any `PUBLIC_*` value is inlined into the client bundle at build time, so changing
|
|
98
|
+
it needs a rebuild, not just a redeploy.
|
|
99
|
+
|
|
100
|
+
## Local loop
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
npm install
|
|
104
|
+
npm run dev # iterate on design
|
|
105
|
+
npm run build && npm run preview
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
To test forms locally, point `PUBLIC_SCRIBE_API_URL` at a local admin
|
|
109
|
+
(`http://localhost:3040`) — but note the endpoint only allows CORS from the
|
|
110
|
+
Site's registered domain, so use the admin's own host or expect to test
|
|
111
|
+
submissions against the deployed site.
|
|
112
|
+
|
|
113
|
+
## Checklist when a deploy looks wrong
|
|
114
|
+
|
|
115
|
+
- Images 404 on the demo URL but work on the custom domain → missing `base`
|
|
116
|
+
prefix (see `collections.md`).
|
|
117
|
+
- Editor published but the site is stale → `workflow_dispatch` missing, or the
|
|
118
|
+
workflow failed; check the Actions tab.
|
|
119
|
+
- Form says "Failed to fetch" → `PUBLIC_SCRIBE_API_URL` baked in wrong, or the
|
|
120
|
+
origin isn't the Site's registered domain.
|
|
121
|
+
- Build fails after an editor added a field → the content zod schema is too
|
|
122
|
+
strict (see `collections.md`).
|