caracms 0.7.0 → 0.8.1
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 +108 -19
- package/bin/caracms.mjs +11 -3
- package/bin/init.mjs +332 -34
- package/bin/media.mjs +60 -6
- package/bin/templates/railway-cloud.ts.txt +73 -0
- package/package.json +1 -1
- package/skills/caracms-adopt/SKILL.md +30 -13
- package/skills/caracms-hosting/SKILL.md +240 -0
- package/skills/caracms-tina/SKILL.md +45 -15
- package/src/body.ts +55 -0
- package/src/cli/validate.ts +1 -1
- package/src/cloud-middleware.ts +51 -5
- package/src/config.ts +20 -1
- package/src/content.ts +15 -6
- package/src/document-problems.ts +48 -0
- package/src/editor/client/collections/CollectionGrid.tsx +2 -2
- package/src/editor/client/collections/CollectionTable.tsx +3 -3
- package/src/editor/client/documents/DocMenu.tsx +56 -1
- package/src/editor/client/documents/actions.ts +30 -1
- package/src/editor/client/documents/dialogs.tsx +61 -3
- package/src/editor/client/editor.ts +98 -2
- package/src/editor/client/fields/LockupField.tsx +4 -4
- package/src/editor/client/inbox/InboxStage.tsx +25 -1
- package/src/editor/client/live/notices.tsx +19 -0
- package/src/editor/client/live/publish.tsx +58 -2
- package/src/editor/client/nav/DocRow.tsx +4 -4
- package/src/editor/client/pane/levels/CollectionLevel.tsx +2 -1
- package/src/editor/client/pane/levels/DocumentLevel.tsx +2 -1
- package/src/editor/client/pane/levels/DocumentsLevel.tsx +2 -2
- package/src/editor/client/pane/levels/FormLevel.tsx +33 -6
- package/src/editor/client/pane/levels/SubmissionsLevel.tsx +19 -2
- package/src/editor/client/pane/parts.tsx +67 -4
- package/src/editor/client/pane/switcher.tsx +4 -4
- package/src/editor/client/shell/Bar.tsx +11 -0
- package/src/editor/client/state/keeper.ts +28 -4
- package/src/editor/client/state/live.ts +18 -1
- package/src/editor/routes/_live.ts +1 -1
- package/src/editor/routes/admin.astro +9 -0
- package/src/editor/routes/discard.ts +11 -0
- package/src/editor/server/admin-props.ts +18 -2
- package/src/editor/server/arrange.ts +4 -4
- package/src/editor/server/create.ts +4 -4
- package/src/editor/server/drafts-store.ts +2 -2
- package/src/editor/server/empty-site.ts +23 -0
- package/src/editor/server/media/cloud-mux.ts +4 -1
- package/src/editor/server/media/config.ts +12 -5
- package/src/editor/server/media/upload.ts +2 -41
- package/src/editor/server/mode.ts +3 -1
- package/src/editor/server/move.ts +32 -4
- package/src/editor/server/write.ts +4 -4
- package/src/editor/styles/levels/live.css +2 -0
- package/src/editor/types.ts +92 -4
- package/src/email.ts +4 -1
- package/src/forms/cloud-store.ts +4 -1
- package/src/forms/inbox.ts +10 -5
- package/src/forms/store.ts +21 -4
- package/src/index.ts +21 -6
- package/src/live/auth/login.ts +4 -3
- package/src/live/auth/views.ts +8 -1
- package/src/live/autosaves.ts +94 -6
- package/src/live/cloud.ts +64 -20
- package/src/live/drafts-store.ts +17 -6
- package/src/live/editor.ts +232 -16
- package/src/live/git.ts +492 -22
- package/src/live/github.ts +84 -17
- package/src/live/locks.ts +22 -0
- package/src/live/messages.ts +49 -2
- package/src/live/mode.ts +19 -8
- package/src/live/notices.ts +5 -1
- package/src/live/runtime.ts +2 -1
- package/src/live/services.ts +114 -0
- package/src/live/status.ts +5 -0
- package/src/live/sweep.ts +44 -1
- package/src/live/webhook.ts +8 -3
- package/src/media.ts +9 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineRailway, github, preserve, project, redis, ref, service } from "railway/iac";
|
|
2
|
+
|
|
3
|
+
// A caracms site's Railway project, as code (written by `caracms init` from the
|
|
4
|
+
// template for a site on CaraCMS Cloud). From the site's root: `railway config
|
|
5
|
+
// plan` shows what would change, `railway config apply` makes it so. Plan
|
|
6
|
+
// first, every time.
|
|
7
|
+
//
|
|
8
|
+
// On CaraCMS Cloud the site's GitHub access, images, forms and email, and video
|
|
9
|
+
// all come through CARACMS_CLOUD_KEY, so the project is the site and its Redis
|
|
10
|
+
// (the editor's drafts and sessions): no buckets, no imgproxy.
|
|
11
|
+
//
|
|
12
|
+
// What this file doesn't do, set by hand on Railway: create the project, the
|
|
13
|
+
// site's domain, Redis's volume and start command (Railway's Redis template
|
|
14
|
+
// makes them), the secrets' values, and the optional secrets below until
|
|
15
|
+
// they're wanted.
|
|
16
|
+
|
|
17
|
+
// ---- The site's names: a new site edits this block and nothing else. ----
|
|
18
|
+
const SITE = "{{SITE}}";
|
|
19
|
+
// The repo Railway builds from (the editor's repo is the one linked in CaraCMS Cloud).
|
|
20
|
+
const REPO = "{{GITHUB_REPO}}";
|
|
21
|
+
// Railway's region for the services.
|
|
22
|
+
const REGION = "europe-west4-drams3a";
|
|
23
|
+
// The site is the repo's root: no watch paths, so every push to main deploys.
|
|
24
|
+
const WATCH_PATHS: string[] = [];
|
|
25
|
+
const BUILD_COMMAND = "{{BUILD}}";
|
|
26
|
+
const START_COMMAND = "{{START}}";
|
|
27
|
+
// -------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export default defineRailway(() => {
|
|
30
|
+
// The editor's drafts and sessions.
|
|
31
|
+
const kv = redis("Redis", { region: REGION });
|
|
32
|
+
// Sleeps when idle. redis() takes only a region, so it's set on the node.
|
|
33
|
+
kv.deploy = { ...kv.deploy, sleepApplication: true };
|
|
34
|
+
|
|
35
|
+
const site = service("site", {
|
|
36
|
+
source: github(REPO, { branch: "main", checkSuites: false }),
|
|
37
|
+
build: { buildCommand: BUILD_COMMAND, watchPatterns: WATCH_PATHS },
|
|
38
|
+
start: START_COMMAND,
|
|
39
|
+
healthcheck: "/health",
|
|
40
|
+
replicas: { [REGION]: 1 },
|
|
41
|
+
deploy: { restartPolicyMaxRetries: 3 },
|
|
42
|
+
env: {
|
|
43
|
+
// ---- Written here: literals and Railway references ----
|
|
44
|
+
NODE_ENV: "production",
|
|
45
|
+
// The public address (canonical links, the sitemap, og:url). At launch it
|
|
46
|
+
// becomes the client's domain, written out as "https://<domain>".
|
|
47
|
+
SITE_URL: "https://${{RAILWAY_PUBLIC_DOMAIN}}",
|
|
48
|
+
REDIS_URL: ref(kv, "REDIS_URL"),
|
|
49
|
+
|
|
50
|
+
// ---- Set by hand on Railway: names only, the values never live here ----
|
|
51
|
+
// On a fresh project these stay unset and the plan doesn't mention them.
|
|
52
|
+
// The site's key from app.caracms.com (a key labelled for this environment).
|
|
53
|
+
CARACMS_CLOUD_KEY: preserve(),
|
|
54
|
+
// The editor's sign-in.
|
|
55
|
+
CARACMS_SECRET: preserve(),
|
|
56
|
+
CARACMS_ALLOW: preserve(),
|
|
57
|
+
CARACMS_BACKUP_USER: preserve(),
|
|
58
|
+
CARACMS_BACKUP_PASSWORD: preserve(),
|
|
59
|
+
// Try again on a failed publish redeploys through this project token.
|
|
60
|
+
CARACMS_RAILWAY_TOKEN: preserve(),
|
|
61
|
+
// Optional. The form's spam check: the site key is baked in at build, so
|
|
62
|
+
// a change needs a redeploy.
|
|
63
|
+
TURNSTILE_SITE_KEY: preserve(),
|
|
64
|
+
TURNSTILE_SECRET_KEY: preserve(),
|
|
65
|
+
// Optional. Pre-launch password ("user:password"); unset means public.
|
|
66
|
+
CARACMS_PRELAUNCH: preserve(),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return project(SITE, {
|
|
71
|
+
resources: [site, kv],
|
|
72
|
+
});
|
|
73
|
+
});
|
package/package.json
CHANGED
|
@@ -22,12 +22,16 @@ Start from a clean git checkout (untracked files are fine). The order is:
|
|
|
22
22
|
(or `pnpm add caracms --save-exact`).
|
|
23
23
|
2. **Commit the install** (`package.json` and the lockfile). `init --write` refuses while
|
|
24
24
|
tracked files have uncommitted changes, so that its own change is one diff to review.
|
|
25
|
-
3. **`npx caracms init
|
|
26
|
-
|
|
25
|
+
3. **`npx caracms init --cloud`** (or `--self-hosted`), the dry run (below), and read it.
|
|
26
|
+
The flag is the site's mode, where its services come from: choosing it is
|
|
27
|
+
`caracms-hosting` → section 1. Always pass it: run without a terminal, `init` won't
|
|
28
|
+
pick a mode, and on a new site it writes nothing and exits 1.
|
|
29
|
+
4. **`npx caracms init --cloud --write`** (or `--self-hosted --write`).
|
|
27
30
|
5. **Install again** (`npm install` / `pnpm install`): `init` adds dependencies to
|
|
28
31
|
`package.json`.
|
|
29
32
|
|
|
30
|
-
**`npx caracms init`** is a dry run: it
|
|
33
|
+
**`npx caracms init --cloud`** (or `--self-hosted`) without `--write` is a dry run: it
|
|
34
|
+
writes nothing and prints
|
|
31
35
|
|
|
32
36
|
- one line per file: `add` (a new file), `edit` (an existing one), `link` / `unlink` (a
|
|
33
37
|
skill link in `.claude/skills/`);
|
|
@@ -49,8 +53,8 @@ adapter in middleware mode; an Astro outside caracms's range, or none; a Tina si
|
|
|
49
53
|
port, not `init`; an untracked leftover `tina/__generated__/` is only a note). `init --write` also refuses with uncommitted changes
|
|
50
54
|
to tracked files: usually the install of step 1, not yet committed.
|
|
51
55
|
|
|
52
|
-
Read the diff. Then **`npx caracms init --write
|
|
53
|
-
`pnpm install`), and commit that as one change.
|
|
56
|
+
Read the diff. Then **`npx caracms init --cloud --write`** (or `--self-hosted --write`),
|
|
57
|
+
install again (`npm install` / `pnpm install`), and commit that as one change.
|
|
54
58
|
|
|
55
59
|
pnpm 12.11 and later may print `Agent skills awaiting approval: caracms` after the install.
|
|
56
60
|
That's pnpm's own gate: it links the skills a dependency ships into `.claude/skills/` only
|
|
@@ -61,7 +65,7 @@ unapproved, and don't approve it in passing (`pnpm approve` also reviews build s
|
|
|
61
65
|
`init` added:
|
|
62
66
|
|
|
63
67
|
- `caracms.config.ts`: a `pages` collection at `/` (nested), one `text` block, no text
|
|
64
|
-
styles;
|
|
68
|
+
styles, and `services: 'cloud'` (or `'self-hosted'`), the mode;
|
|
65
69
|
- `src/components/caracms/Page.astro` (the page component) and `TextBlock.astro`;
|
|
66
70
|
- `src/pages/[...path].astro`, the one catch-all, which serves every document with an
|
|
67
71
|
address (the pages today; a collection added later needs no route of its own, section
|
|
@@ -75,7 +79,7 @@ unapproved, and don't approve it in passing (`pnpm approve` also reviews build s
|
|
|
75
79
|
- the skill links and a block in `CLAUDE.md`;
|
|
76
80
|
- with `--railway` only, `.railway/railway.ts` (when the site had none) and `railway`
|
|
77
81
|
(dev). Without it, `init` prints the variables any host needs instead. Where and how
|
|
78
|
-
the site runs (Railway, Render, Fly, a VPS) is
|
|
82
|
+
the site runs (Railway, Render, Fly, a VPS) is `caracms-hosting` → section 2.
|
|
79
83
|
|
|
80
84
|
Then `astro build`: every existing page must still build. The pages stay prerendered, but
|
|
81
85
|
the output moves to `dist/client/` and `dist/server/`, and `npm start` is now
|
|
@@ -83,8 +87,9 @@ the output moves to `dist/client/` and `dist/server/`, and `npm start` is now
|
|
|
83
87
|
(its own built-site checks, a script) needs the new paths: the pages are in
|
|
84
88
|
`dist/client/`.
|
|
85
89
|
|
|
86
|
-
Re-run `init` after updating caracms: it
|
|
87
|
-
block and the schemas. On a set-up site with nothing
|
|
90
|
+
Re-run `init` after updating caracms: it reads the mode from the config, re-links the
|
|
91
|
+
skills and refreshes the CLAUDE.md block and the schemas. On a set-up site with nothing
|
|
92
|
+
to do it says
|
|
88
93
|
`caracms init: nothing to do. caracms is set up here.`
|
|
89
94
|
|
|
90
95
|
## 2. Set up the comparison before changing a page
|
|
@@ -107,9 +112,12 @@ are `content/pages/<path>.json` and `content/<collection>/<slug>.json`, the buil
|
|
|
107
112
|
`dist/client/`), not what they check. A check that a page's own CSS isn't on another page
|
|
108
113
|
is the exception (section 4).
|
|
109
114
|
|
|
110
|
-
**Images need
|
|
111
|
-
`
|
|
112
|
-
|
|
115
|
+
**Images need credentials** in the site's `.env` (never committed). On CaraCMS Cloud
|
|
116
|
+
that's `CARACMS_CLOUD_KEY` alone: a key labelled for this laptop, made in
|
|
117
|
+
app.caracms.com (`caracms-hosting` → section 1); it shows, imports and uploads images,
|
|
118
|
+
and makes `astro dev` live on the site's real repo. Self-hosted, it's the media
|
|
119
|
+
variables: `CARACMS_MEDIA_URL` (imgproxy's origin), `IMGPROXY_KEY`, `IMGPROXY_SALT` and
|
|
120
|
+
`S3_BUCKET` to show images, plus `S3_ENDPOINT`, `S3_REGION`, `S3_ACCESS_KEY_ID` and
|
|
113
121
|
`S3_SECRET_ACCESS_KEY` to import and upload them. Without them `astro dev` draws a
|
|
114
122
|
caracms image as nothing, and `astro build` stops with a sentence naming what's missing.
|
|
115
123
|
If they aren't there, say so; don't invent values.
|
|
@@ -333,7 +341,8 @@ editor's uploads are) and prints one JSON array on stdout:
|
|
|
333
341
|
- It reads `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID` and
|
|
334
342
|
`S3_SECRET_ACCESS_KEY` from the environment, else the site's `.env`. On CaraCMS Cloud it
|
|
335
343
|
reads `CARACMS_CLOUD_KEY` instead (the same places) and uploads to the site's cloud
|
|
336
|
-
folder, with the same output
|
|
344
|
+
folder, with the same output. It goes by the config's `services`: the other mode's
|
|
345
|
+
variables, if set, are ignored with one warning naming them.
|
|
337
346
|
- The same image again writes nothing and prints its key with `"existed": true`. It never
|
|
338
347
|
deletes anything.
|
|
339
348
|
- JPEG, PNG, WebP, AVIF, HEIC/HEIF, GIF and SVG, up to 40 MB and 50 megapixels. If any file
|
|
@@ -527,3 +536,11 @@ node .claude/skills/caracms-adopt/look-same.mjs --playwright /tmp/look-same \
|
|
|
527
536
|
|
|
528
537
|
Work on a branch and open a pull request; never commit to `main` directly, and never
|
|
529
538
|
touch the `drafts` branch.
|
|
539
|
+
|
|
540
|
+
## 11. Before the first deploy: `caracms-hosting`
|
|
541
|
+
|
|
542
|
+
The site's hosting is `caracms-hosting`'s, not this skill's. Before the site is first
|
|
543
|
+
deployed with caracms, `init` must have run with the mode (`--cloud` or `--self-hosted`,
|
|
544
|
+
section 1), so `caracms.config.ts` has `services`; then follow `caracms-hosting`: the
|
|
545
|
+
host for the server (section 2), and, for a site that ran self-hosted and is going onto
|
|
546
|
+
the cloud, the move list `init --cloud` prints (section 3).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caracms-hosting
|
|
3
|
+
description: Set up where a caracms site runs — choose its mode (CaraCMS Cloud or self-hosted, the `services` field in caracms.config.ts) and run `caracms init --cloud` or `--self-hosted`, put its server on a host (Railway through `init --railway`; Render, Fly or a VPS by hand), move a self-hosted site onto the cloud by following `init`'s move list in order, and read the cloud's refusals (a storage or video limit, the daily email limit, a switched-off team, a revoked key, the GitHub App not installed) with what to do for each. Use before a site's first deploy, when moving a site between modes, when changing its host, or when the cloud says no.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Where a caracms site runs
|
|
7
|
+
|
|
8
|
+
A site makes three choices, and they're separate:
|
|
9
|
+
|
|
10
|
+
- **Where it starts** (Tina, an existing Astro site, …) decides which skill converts it:
|
|
11
|
+
`caracms-tina`, `caracms-adopt`. Those skills hand over to this one before the first
|
|
12
|
+
deploy.
|
|
13
|
+
- **Its mode**, where its services come from (GitHub access, media, forms and email,
|
|
14
|
+
video): **CaraCMS Cloud** or **self-hosted**. Section 1.
|
|
15
|
+
- **Where its server runs**: a cloud site still runs its own Node server and Redis
|
|
16
|
+
somewhere. Section 2.
|
|
17
|
+
|
|
18
|
+
**The config says the mode; the variables don't.** `services` in `caracms.config.ts`
|
|
19
|
+
(`'cloud'` or `'self-hosted'`, inside `defineSite({ … })`) is the one source of truth.
|
|
20
|
+
Variables from the other mode are leftovers to retire, never a second opinion: with the
|
|
21
|
+
field set, the site ignores them and says so once at build, once at the server's first
|
|
22
|
+
request and once from `media import`:
|
|
23
|
+
|
|
24
|
+
> S3_BUCKET is set, but this site runs on CaraCMS Cloud (caracms.config.ts): it's ignored.
|
|
25
|
+
> Remove it once the old bucket and imgproxy are retired.
|
|
26
|
+
|
|
27
|
+
A config with no `services` field behaves as caracms did before the field existed (the
|
|
28
|
+
key decides, and the key beside the site's own variables is refused). The next `init`
|
|
29
|
+
with a mode adds the field.
|
|
30
|
+
|
|
31
|
+
Detail lives in the caracms README (`node_modules/caracms/README.md`): **Hosting** (what
|
|
32
|
+
every host needs, the variables, each host) and **Hosting → CaraCMS Cloud** (the key,
|
|
33
|
+
images, forms and email, video, `media push`, working locally). This skill is the order
|
|
34
|
+
to do things in; read those sections for the rest, rather than guessing.
|
|
35
|
+
|
|
36
|
+
Commands run from the site's root: `npx caracms …`, or `pnpm caracms …` in a pnpm site.
|
|
37
|
+
|
|
38
|
+
## 1. Choose the mode and run `init` with it
|
|
39
|
+
|
|
40
|
+
| Mode | The site's server needs | On its host |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **CaraCMS Cloud** | `CARACMS_CLOUD_KEY`, plus the site's own `SITE_URL`, `REDIS_URL`, `CARACMS_SECRET`, sign-in (`CARACMS_ALLOW` and/or the backup sign-in), Turnstile | the site and Redis |
|
|
43
|
+
| **Self-hosted** | a GitHub token or App and `GITHUB_REPO`, the media bucket and imgproxy, a submissions bucket, Resend, Mux, plus everything in the row above but the key | the site, Redis, two buckets, imgproxy |
|
|
44
|
+
|
|
45
|
+
The full list, per variable, is the README's *Hosting → The variables*.
|
|
46
|
+
|
|
47
|
+
**CaraCMS Cloud** is for a site CaraCMS runs: it has a team and a site in
|
|
48
|
+
app.caracms.com, its repo linked there, and the caracms GitHub App installed on that
|
|
49
|
+
repo. **Self-hosted** is for a site that keeps its own buckets, imgproxy, Resend and
|
|
50
|
+
GitHub credential. If the brief or the person hasn't said which, ask. Don't pick one
|
|
51
|
+
from what variables happen to be in `.env`.
|
|
52
|
+
|
|
53
|
+
**Always pass the flag.** `init` asks for the mode only when a person is at a terminal;
|
|
54
|
+
run by an agent or a script, it won't pick one. On a new site it writes nothing, exits 1
|
|
55
|
+
and says to run it again with `--cloud` or `--self-hosted`; on a site already set up it
|
|
56
|
+
does everything else, leaves the field out and says "Mode not set: run
|
|
57
|
+
`caracms init --cloud` or `--self-hosted`."
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npx caracms init --cloud # the dry run: read it
|
|
61
|
+
npx caracms init --cloud --write # writes services: 'cloud' and the rest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(`--self-hosted` the same way. Both flags at once is a problem.)
|
|
65
|
+
|
|
66
|
+
- `init` writes the field as the first property of the `defineSite({` literal, or
|
|
67
|
+
replaces an existing `services` property. If the config's `defineSite` argument isn't
|
|
68
|
+
a literal, it writes nothing and names the line to add by hand: add it, then run `init`
|
|
69
|
+
again.
|
|
70
|
+
- A second `init` reads the mode from the config and asks nothing; it re-links the
|
|
71
|
+
skills and refreshes the CLAUDE.md block (whose first line says where the site runs)
|
|
72
|
+
and the schemas. Run it after every caracms update.
|
|
73
|
+
- **A flag that contradicts the field is a move**, not an error: `--cloud` on a
|
|
74
|
+
self-hosted site prints the move list (section 3); `--self-hosted` on a cloud site
|
|
75
|
+
prints the way back (section 3, the end).
|
|
76
|
+
- **`init --cloud` checks the key** when it finds one (the environment, then `.env`): it
|
|
77
|
+
shows the site and team the key belongs to, the linked repo, and whether the App is
|
|
78
|
+
installed on it, or the cloud's own sentence for a refused key (section 4). No key: it
|
|
79
|
+
says where to make one and carries on. **Never print, paste or commit a key.**
|
|
80
|
+
|
|
81
|
+
**One key per place the site runs**, each labelled in app.caracms.com: "Railway" for
|
|
82
|
+
the host, "Rich's laptop" for a laptop's `.env`, "staging" for a staging deploy.
|
|
83
|
+
Revoking one never touches the others. Leave `CARACMS_CLOUD_URL` unset (it's only for
|
|
84
|
+
working on CaraCMS Cloud itself). On a laptop a cloud key makes `astro dev` live on the
|
|
85
|
+
site's real repo and folder; without one, `astro dev` edits content on disk as always.
|
|
86
|
+
|
|
87
|
+
**A production build needs the key.** With `services: 'cloud'` and no
|
|
88
|
+
`CARACMS_CLOUD_KEY`, `astro build` (and `pnpm build` on a laptop, which is a production
|
|
89
|
+
build) fails, and a production server's `/admin` answers 503, with:
|
|
90
|
+
|
|
91
|
+
> This site runs on CaraCMS Cloud (caracms.config.ts), but CARACMS_CLOUD_KEY isn't set.
|
|
92
|
+
> Make a key for it in app.caracms.com.
|
|
93
|
+
|
|
94
|
+
It never falls back to self-hosted variables that happen to be set.
|
|
95
|
+
|
|
96
|
+
## 2. Where the server runs
|
|
97
|
+
|
|
98
|
+
Every host needs what the README's *Hosting* opens with: one instance, HTTPS, a deploy
|
|
99
|
+
whenever `main` changes, `SITE_URL` the same at build and run time, and the server
|
|
100
|
+
started with `caracms start`. The mode only changes which variables go on it (section 1).
|
|
101
|
+
Off Railway, Fly and Render, `CARACMS_CLIENT_IP_HEADER` is required too (*Hosting → The
|
|
102
|
+
visitor's IP*).
|
|
103
|
+
|
|
104
|
+
Each host below is its own section. **A new host is a new section here**, written the
|
|
105
|
+
same way: how it's set up, the variables per mode, anything it does differently. The
|
|
106
|
+
rest of this skill doesn't change for it.
|
|
107
|
+
|
|
108
|
+
### Railway
|
|
109
|
+
|
|
110
|
+
**Set up by `init`.** `init --railway` writes `.railway/railway.ts`, the file that
|
|
111
|
+
describes the whole Railway project, when the site has none:
|
|
112
|
+
|
|
113
|
+
- `--cloud --railway`: the site and Redis, nothing else.
|
|
114
|
+
- `--self-hosted --railway`: the site, Redis, the media and submissions buckets, and
|
|
115
|
+
imgproxy.
|
|
116
|
+
|
|
117
|
+
Then the README's *Hosting → Railway* (the IP header and commit come by default; "Try
|
|
118
|
+
again" uses `CARACMS_RAILWAY_TOKEN`). Set `CARACMS_CLOUD_KEY` (the "Railway" key) on the
|
|
119
|
+
site's service on a cloud site.
|
|
120
|
+
|
|
121
|
+
**`init` never rewrites an existing `.railway/railway.ts`.** `railway config apply`
|
|
122
|
+
makes the project match the file, deleting what the file no longer declares. So:
|
|
123
|
+
|
|
124
|
+
- **Never run `railway config apply` on a file cut down to the site and Redis before
|
|
125
|
+
`caracms media push` has finished** (section 3, step 3): it would delete the media
|
|
126
|
+
bucket the images are still being copied from.
|
|
127
|
+
- Never apply a file you haven't read against the environment it would change. A
|
|
128
|
+
production file a template wrote (a Tina template site's) can switch its CDN off; set
|
|
129
|
+
production's variables with `railway variables --set` instead.
|
|
130
|
+
- Before any apply, `railway config plan` shows what it would create, change or delete.
|
|
131
|
+
Read it.
|
|
132
|
+
|
|
133
|
+
### Render
|
|
134
|
+
|
|
135
|
+
By hand, from the README's *Hosting → Render*: one instance (no autoscaling), the IP
|
|
136
|
+
default to check, the commit Render sets. Put the mode's variables (section 1) on the
|
|
137
|
+
web service, Redis beside it (*Hosting → Redis*). A self-hosted site also needs its two
|
|
138
|
+
buckets and an imgproxy somewhere the server can reach.
|
|
139
|
+
|
|
140
|
+
### Fly
|
|
141
|
+
|
|
142
|
+
By hand, from the README's *Hosting → Fly*: one machine, a GitHub Action to deploy on a
|
|
143
|
+
push to `main`, the IP header. The mode's variables as Fly secrets; Redis (Upstash works,
|
|
144
|
+
in the site's region). A self-hosted site needs its buckets and imgproxy elsewhere.
|
|
145
|
+
|
|
146
|
+
### A VPS
|
|
147
|
+
|
|
148
|
+
By hand, from the README's *Hosting → A VPS, behind Caddy or nginx*: the proxy,
|
|
149
|
+
`CARACMS_CLIENT_IP_HEADER` set to match it, a deploy on a push to `main`, Redis. The
|
|
150
|
+
mode's variables in the server's environment.
|
|
151
|
+
|
|
152
|
+
## 3. The move: self-hosted to CaraCMS Cloud
|
|
153
|
+
|
|
154
|
+
**Follow the list `init --cloud` prints, in its order.** On a site that's self-hosted
|
|
155
|
+
(the field says so, or there's none and the site has a `.railway/railway.ts` with
|
|
156
|
+
buckets or imgproxy, or media, video, forms or GitHub variables in `.env`),
|
|
157
|
+
`npx caracms init --cloud --write` writes `services: 'cloud'` and prints the steps to
|
|
158
|
+
finish the move, each with the check that says it worked. Only steps that apply are
|
|
159
|
+
printed. Don't reorder them, skip a check, or fit a list of your own to the site.
|
|
160
|
+
|
|
161
|
+
**`init` only sees the repo.** The list names every variable the cloud replaces and marks
|
|
162
|
+
the ones it found in `.env` or `railway.ts`; the host may have more. Open the host's
|
|
163
|
+
variables page and check it against the full list.
|
|
164
|
+
|
|
165
|
+
The replaced variables:
|
|
166
|
+
|
|
167
|
+
- media: `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID`,
|
|
168
|
+
`S3_SECRET_ACCESS_KEY`, `IMGPROXY_KEY`, `IMGPROXY_SALT`, `CARACMS_MEDIA_URL`
|
|
169
|
+
- video: `MUX_TOKEN_ID`, `MUX_TOKEN_SECRET`
|
|
170
|
+
- forms and email: `SUBMISSIONS_S3_BUCKET`, `SUBMISSIONS_S3_ACCESS_KEY_ID`,
|
|
171
|
+
`SUBMISSIONS_S3_SECRET_ACCESS_KEY`, `RESEND_API_KEY`, `FORMS_FROM`
|
|
172
|
+
- GitHub: `CARACMS_GITHUB_TOKEN`, `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_REPO`
|
|
173
|
+
|
|
174
|
+
(`GITHUB_WEBHOOK_SECRET` isn't on it: the cloud has no webhook, so it's unused and
|
|
175
|
+
harmless.)
|
|
176
|
+
|
|
177
|
+
The list's steps, for orientation (what `init` prints is the one to follow):
|
|
178
|
+
|
|
179
|
+
1. The site and a key in app.caracms.com, the App on the repo, then open the site's page
|
|
180
|
+
in app.caracms.com (the cloud refreshes the install state only when that page opens, a
|
|
181
|
+
repo is linked or the site mints a token). *Check:* `init --cloud` again shows "App
|
|
182
|
+
installed".
|
|
183
|
+
2. Commit and push the field. **The host's build then fails with "CARACMS_CLOUD_KEY isn't
|
|
184
|
+
set" until step 4.** That's expected: the old deployment keeps serving. Don't "fix"
|
|
185
|
+
it by reverting the field.
|
|
186
|
+
3. Copy the images: `caracms media push`, with the old bucket's `S3_*` on its command line
|
|
187
|
+
(README: *Moving a site's images in*). *Check:* `failed: 0`, and a second run skips
|
|
188
|
+
everything. It stops at the storage limit with the cloud's sentence (section 4).
|
|
189
|
+
4. Set `CARACMS_CLOUD_KEY` on the host and **leave the old variables in place** (they're
|
|
190
|
+
ignored). *Check:* the deploy's logs name them as ignored; the editor signs in; an
|
|
191
|
+
upload lands in the cloud.
|
|
192
|
+
5. Video, if `MUX_TOKEN_*` was found: each clip uploaded again in the editor and placed
|
|
193
|
+
where the old one was (no copy command; old clips keep playing until replaced).
|
|
194
|
+
*Check:* no page plays from the old account.
|
|
195
|
+
6. Remove the replaced variables from the host, one by one. *Check:* the next deploy logs
|
|
196
|
+
no "ignored" lines.
|
|
197
|
+
7. Retire, **only once 4–6 hold**: imgproxy, the media bucket, the GitHub token or App on
|
|
198
|
+
this repo, Resend. **Keep the submissions bucket** until its old submissions are
|
|
199
|
+
exported or no longer wanted (the cloud doesn't copy them, and its inbox doesn't show
|
|
200
|
+
them), and **keep the old Mux account** until step 5 is finished.
|
|
201
|
+
8. Cut `.railway/railway.ts` down to the site and Redis (what `--cloud --railway` writes,
|
|
202
|
+
keeping the site's own names). *Check:* `railway config plan` shows nothing to create
|
|
203
|
+
or change.
|
|
204
|
+
|
|
205
|
+
**Retiring can't be undone by git.** Deleting a bucket, a service or an account is the
|
|
206
|
+
person's call: say what's ready to go and let them do it, or do it when they say so.
|
|
207
|
+
Everything before step 7 can be reverted with git and the host's variables.
|
|
208
|
+
|
|
209
|
+
**The way back** (cloud to self-hosted): `npx caracms init --self-hosted --write` writes
|
|
210
|
+
the field and prints the README's leaving notes: copy the site's cloud folder to a bucket
|
|
211
|
+
of your own (the keys in content stay the same), set the self-hosted variables, and stand
|
|
212
|
+
up what the mode needs on the host (section 2).
|
|
213
|
+
|
|
214
|
+
## 4. When the cloud says no
|
|
215
|
+
|
|
216
|
+
The cloud refuses in its own sentence, and caracms shows that sentence as it is: in the
|
|
217
|
+
build log, on `/admin` (a 503, with the public site still serving), in the editor while
|
|
218
|
+
it's open, in `media push` / `media import` output, or on a submission in the inbox.
|
|
219
|
+
**Never answer a refusal by setting the site's own variables:** with
|
|
220
|
+
`services: 'cloud'` they're ignored.
|
|
221
|
+
|
|
222
|
+
Who's who: **CaraCMS** runs the cloud (staff, in admin.caracms.com); the **site's team**
|
|
223
|
+
owns the site in app.caracms.com (its keys, its repo). Limits and switching a team on
|
|
224
|
+
are CaraCMS's; keys and the repo are the team's.
|
|
225
|
+
|
|
226
|
+
| The sentence (its code) | What to do |
|
|
227
|
+
|---|---|
|
|
228
|
+
| "This site has reached its storage limit in CaraCMS Cloud (…). Ask CaraCMS to raise it." (`upload_limit`) | Uploads and `media push` stop there. Tell the person: CaraCMS raises the limit. Then run `media push` again (it skips what's copied). Don't delete images to make room. |
|
|
229
|
+
| "This site has reached its video limit in CaraCMS Cloud (…). Ask CaraCMS to raise it." (`video_limit`) | New clips are refused; existing ones play. CaraCMS raises it. A clip counts once Mux has finished it, so one upload can go past the limit. |
|
|
230
|
+
| "Not emailed: this site reached its daily email limit in CaraCMS Cloud (…). Ask CaraCMS to raise it." (on a submission in the inbox) | The submission is stored and readable in the inbox; its email is never sent later. The limit is 50 a day (UTC) unless CaraCMS has set another, and CaraCMS can raise it. For a test, a laptop never emails (submissions go to `.caracms/submissions/`, emails to the log). |
|
|
231
|
+
| "This site has sent the most sign-in emails CaraCMS Cloud sends in a day. Try again tomorrow." (`sign_in_limit`) | Sign-in links stop until the next day. The backup sign-in (`CARACMS_BACKUP_USER` / `_PASSWORD`) still works, if it's set. |
|
|
232
|
+
| "This site's team is switched off. Ask CaraCMS to switch it back on." (`team_off`) | Builds fail, `/admin` answers 503, the running site's public pages keep serving. Only CaraCMS can switch it back on; tell the person. |
|
|
233
|
+
| "That site key was revoked. Make a new one in app.caracms.com." (`revoked_key`) | Make a new labelled key for that place (host, laptop, staging), set it where the old one was (the host's variable or `.env`), redeploy or restart. Don't reuse another place's key. |
|
|
234
|
+
| "That site key isn't one we know." (`unknown_key`) | A mistyped or truncated key, a key for a deleted site, or `CARACMS_CLOUD_URL` pointing at another cloud. Check the variable's value has no stray quotes or spaces, unset `CARACMS_CLOUD_URL`, else make a new key. |
|
|
235
|
+
| "The caracms GitHub App isn't installed on <repo>, or that repo doesn't exist. Install the App on it: <link>" (`app_not_installed`) | Install the App from the link on that repo only. The cloud refreshes its record of the install only when the site's page in app.caracms.com opens (or a repo is linked, or the site mints a token), so open the site's page there, then run `init --cloud` again: it shows "App installed". The next visit to `/admin` tries again. If it is installed, the repo linked in app.caracms.com is the wrong one: the team fixes it there (a changed repo reaches the site at its next start). |
|
|
236
|
+
| "This site has no GitHub repo in CaraCMS Cloud yet. A team owner can pick one in app.caracms.com." (`no_repo`; what `/admin` shows) | A team owner links the repo in app.caracms.com, then the App goes on it. The site still builds meanwhile. |
|
|
237
|
+
| "CaraCMS Cloud couldn't be reached, so the site can't be built. Try again in a minute." (`unreachable`) | Retry the build. A running site keeps serving. If it persists, tell the person; don't switch the mode. |
|
|
238
|
+
| "This site runs on CaraCMS Cloud (caracms.config.ts), but CARACMS_CLOUD_KEY isn't set. Make a key for it in app.caracms.com." | Not the cloud refusing: the key is missing where the site is built or runs (section 1). Expected during the move's steps 2–3. |
|
|
239
|
+
|
|
240
|
+
The README's *Hosting → CaraCMS Cloud* has each of these in context.
|
|
@@ -21,6 +21,8 @@ Read with this skill:
|
|
|
21
21
|
preview (section 8). This skill is the Tina-specific order; adopt's page-by-page
|
|
22
22
|
method doesn't apply, since the script converts every page at once.
|
|
23
23
|
- `caracms-content`: the content files the script writes.
|
|
24
|
+
- `caracms-hosting`: the mode `init` is run with, the host, and (on CaraCMS Cloud) the
|
|
25
|
+
move list to follow at the switch. Hosting is that skill's, not this one's.
|
|
24
26
|
- Beside this file: **`tina-to-caracms.mjs`**, the reference script (its header is its
|
|
25
27
|
manual), and **`field-kinds.md`**, the mapping table: how each Tina field kind becomes a
|
|
26
28
|
caracms field and value.
|
|
@@ -64,9 +66,13 @@ Work on a branch (`caracms-port`, say) in a worktree of the site. Commit after e
|
|
|
64
66
|
3. **Delete `content/`** (git keeps it). `init` refuses a `content/` that isn't caracms's.
|
|
65
67
|
4. **Switch the adapter to standalone:** `@astrojs/node` with `mode: 'standalone'`.
|
|
66
68
|
`caracms start` serves `dist/server/entry.mjs` and refuses any other mode.
|
|
67
|
-
5. **`npx caracms init
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
5. **`npx caracms init --cloud`** (or `--self-hosted`), read the dry run, then
|
|
70
|
+
**`npx caracms init --cloud --write`** (or `--self-hosted --write`) and install again
|
|
71
|
+
(caracms-adopt → section 1). The flag is the site's mode: choosing it is
|
|
72
|
+
`caracms-hosting` → section 1, and it's always passed (without a terminal `init`
|
|
73
|
+
won't pick one). It links the skills, writes the config (with `services`), the
|
|
74
|
+
CLAUDE.md block and the health route. On a site with a template's `railway.ts`,
|
|
75
|
+
`--cloud` also prints the move list: hold it for the switch (section 9). **A template site already has a
|
|
70
76
|
`.railway/railway.ts` that describes production: leave it alone and never apply it**
|
|
71
77
|
(applying it can switch production's CDN off). `init` treats a site that has one as
|
|
72
78
|
a Railway site and leaves the file untouched, which is the expected outcome. Set the
|
|
@@ -366,16 +372,30 @@ in the editor and in `validate`, naming the page.
|
|
|
366
372
|
## 7. Images
|
|
367
373
|
|
|
368
374
|
- **Keep the bucket keys.** The template's images are imgproxy URLs over the media bucket
|
|
369
|
-
(`…/unsafe/plain/s3://<bucket>/uploads/…`); the script keeps each `uploads/…` key, so
|
|
370
|
-
|
|
375
|
+
(`…/unsafe/plain/s3://<bucket>/uploads/…`); the script keeps each `uploads/…` key, so the
|
|
376
|
+
content never needs rewriting for where the images live, and the bucket only grows.
|
|
371
377
|
- **`w`/`h` are found once** and cached in `scripts/tina-media-sizes.json` (commit it). The
|
|
372
378
|
first run needs the images reachable (their Tina URLs) or the bucket (`S3_*`); after
|
|
373
379
|
that the cache answers. `validate` warns on any image still missing its size.
|
|
374
|
-
|
|
380
|
+
Where the images are served from then depends on the mode (`services`, section 1 step 5):
|
|
381
|
+
|
|
382
|
+
- **On CaraCMS Cloud:** the keys are kept and `caracms media push` copies them, under the
|
|
383
|
+
same `uploads/…` keys, from the template's bucket into the site's cloud folder (README:
|
|
384
|
+
*Moving a site's images in*; `caracms-hosting` → section 3, step 3). The content doesn't
|
|
385
|
+
change, and the cloud serves them signed from `img.caracms.cloud`, so **no signed
|
|
386
|
+
imgproxy goes up beside the old one**. Run the push with the bucket's `S3_*` on its
|
|
387
|
+
command line before the branch environment's first build that shows images, and again
|
|
388
|
+
before the switch for anything uploaded in Tina since (it skips what's copied).
|
|
389
|
+
`CARACMS_CLOUD_KEY` alone shows images (a key per place: the laptop, the branch
|
|
390
|
+
environment). The template's own imgproxy keeps serving Tina's site untouched until
|
|
391
|
+
the switch.
|
|
392
|
+
- **Self-hosted:** caracms signs every URL (`CARACMS_MEDIA_URL`, `IMGPROXY_KEY`,
|
|
375
393
|
`IMGPROXY_SALT`, `S3_BUCKET`). The template's production imgproxy is unsigned and its
|
|
376
394
|
URLs are cached by the CDN, so **never give it a key while Tina runs**: stand up a new,
|
|
377
|
-
signed imgproxy beside it, reading the same bucket.
|
|
378
|
-
|
|
395
|
+
signed imgproxy beside it, reading the same bucket.
|
|
396
|
+
|
|
397
|
+
In both, without the credentials (the key, or the media variables) the preview draws no
|
|
398
|
+
images, and a build stops with a sentence naming what's missing.
|
|
379
399
|
|
|
380
400
|
## 8. Forms
|
|
381
401
|
|
|
@@ -413,19 +433,26 @@ The port is deployed from its branch to its own environment, **never production*
|
|
|
413
433
|
template sites run on Railway, so this section is written for it. A site on another host
|
|
414
434
|
needs the same pieces there (the caracms README's Hosting section has each host):
|
|
415
435
|
|
|
416
|
-
- A new environment in the site's Railway project (`caracms`), with its own `web
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
436
|
+
- A new environment in the site's Railway project (`caracms`), with its own `web` and
|
|
437
|
+
Redis, deploying from the port branch, and `CARACMS_ALLOW` from the template's admin
|
|
438
|
+
allow list. Then, by mode:
|
|
439
|
+
- **On CaraCMS Cloud:** a key of its own, labelled for the environment in
|
|
440
|
+
app.caracms.com, as `CARACMS_CLOUD_KEY`; no imgproxy, buckets, Resend, Mux or GitHub
|
|
441
|
+
variables (the key replaces them). Its images are the ones `media push` copied
|
|
442
|
+
(section 7).
|
|
443
|
+
- **Self-hosted:** a signed imgproxy and its own submissions bucket. It reads the live
|
|
444
|
+
media bucket (caracms only ever adds to a bucket). Its variables copy production's
|
|
445
|
+
where they carry over (`RESEND_API_KEY`, `FORMS_FROM`, the site's `MUX_TOKEN_*`).
|
|
421
446
|
- **`CARACMS_MAIN_BRANCH` and `CARACMS_DRAFTS_BRANCH`** name caracms's two branches
|
|
422
447
|
(default `main` and `drafts`). Set them to the port branch and its own drafts branch
|
|
423
448
|
(`caracms-port`, `caracms-drafts`) **before anyone signs in**: a Publish there then
|
|
424
449
|
commits to the port branch and redeploys the environment, and the live site's `main`
|
|
425
450
|
never moves. With the defaults, the first Publish would land caracms's content on the
|
|
426
451
|
live site's `main`.
|
|
427
|
-
-
|
|
428
|
-
on the site's repo, or the App (the caracms README → Hosting →
|
|
452
|
+
- Self-hosted, a GitHub credential on the environment, as for any caracms site: a
|
|
453
|
+
fine-grained token on the site's repo, or the App (the caracms README → Hosting →
|
|
454
|
+
GitHub). On the cloud, the caracms App on the repo covers it (`caracms-hosting` →
|
|
455
|
+
section 1).
|
|
429
456
|
- Convert with `--recipients <agency address>`, so test submissions never reach the client.
|
|
430
457
|
- **Never `railway config apply` against production**: production's variables are set
|
|
431
458
|
with `railway variables --set` (a template's Railway file can switch its CDN off).
|
|
@@ -433,6 +460,9 @@ needs the same pieces there (the caracms README's Hosting section has each host)
|
|
|
433
460
|
**The switch** (its own task, later): Tina is frozen, the script is re-run on the latest
|
|
434
461
|
`main` without `--recipients`, the branch is merged, production's variables move to
|
|
435
462
|
caracms's (the branch variables unset), and Tina's imgproxy and variables retire.
|
|
463
|
+
Production's hosting is `caracms-hosting`'s: before the merge deploys, follow it (the
|
|
464
|
+
host, section 2; on the cloud, the move list `init --cloud` printed, in its order,
|
|
465
|
+
retiring nothing until its checks hold, section 3).
|
|
436
466
|
|
|
437
467
|
## 10. Before calling it done
|
|
438
468
|
|
package/src/body.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a request's body under a cap (caracms-cloud adversarial review, finding 10). The
|
|
3
|
+
* bytes are counted as they're read and the read stops the moment they pass the cap,
|
|
4
|
+
* whatever `Content-Length` said or whether it was sent at all (a chunked body has none),
|
|
5
|
+
* so no route buffers more than its cap plus one chunk. A `Content-Length` over the cap is
|
|
6
|
+
* refused before a byte is read. The media upload and the forms endpoint read this way
|
|
7
|
+
* (M5b Decision 6); so do the webhook, sign-in and every editor write route since finding 10.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface CappedBody {
|
|
11
|
+
ok: true;
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export interface OverCap {
|
|
15
|
+
ok: false;
|
|
16
|
+
/** The size, when the request said it (Content-Length); null when the read was stopped without knowing. */
|
|
17
|
+
size: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read a request's body, stopping as soon as it passes `cap` bytes. */
|
|
21
|
+
export async function readCapped(request: Request, cap: number): Promise<CappedBody | OverCap> {
|
|
22
|
+
const declared = Number(request.headers.get('content-length') ?? NaN);
|
|
23
|
+
const said = Number.isFinite(declared) && declared >= 0 ? declared : null;
|
|
24
|
+
if (said !== null && said > cap) {
|
|
25
|
+
await request.body?.cancel().catch(() => {});
|
|
26
|
+
return { ok: false, size: said };
|
|
27
|
+
}
|
|
28
|
+
const reader = request.body?.getReader();
|
|
29
|
+
if (!reader) return { ok: true, bytes: new Uint8Array(0) };
|
|
30
|
+
const chunks: Uint8Array[] = [];
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done) break;
|
|
35
|
+
total += value.byteLength;
|
|
36
|
+
if (total > cap) {
|
|
37
|
+
await reader.cancel().catch(() => {});
|
|
38
|
+
return { ok: false, size: said !== null && said > total ? said : null };
|
|
39
|
+
}
|
|
40
|
+
chunks.push(value);
|
|
41
|
+
}
|
|
42
|
+
const bytes = new Uint8Array(total);
|
|
43
|
+
let at = 0;
|
|
44
|
+
for (const c of chunks) {
|
|
45
|
+
bytes.set(c, at);
|
|
46
|
+
at += c.byteLength;
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, bytes };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The body as UTF-8 text, or null when it passes `cap` bytes (the route answers its own 413). */
|
|
52
|
+
export async function readCappedText(request: Request, cap: number): Promise<string | null> {
|
|
53
|
+
const read = await readCapped(request, cap);
|
|
54
|
+
return read.ok ? new TextDecoder().decode(read.bytes) : null;
|
|
55
|
+
}
|
package/src/cli/validate.ts
CHANGED
|
@@ -424,7 +424,7 @@ export async function validateSite(site: Site, codeRoutes: readonly CodeRoute[]
|
|
|
424
424
|
const c: Context = { file: entry.file, site, report, warningContext, exists };
|
|
425
425
|
const collection = site.collections[entry.collection]!;
|
|
426
426
|
if (entry.warning) {
|
|
427
|
-
if (entry.warning.code === 'no-title') error(c, 'title', 'This document has no "title", so
|
|
427
|
+
if (entry.warning.code === 'no-title') error(c, 'title', 'This document has no "title", so it has no page on the site. Add one.');
|
|
428
428
|
else error(c, '', entry.warning.message);
|
|
429
429
|
if (entry.warning.code !== 'no-title') continue;
|
|
430
430
|
}
|