caracms 0.6.0 → 0.8.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 CHANGED
@@ -27,8 +27,10 @@ one diff to review. Then look at what `init` would do:
27
27
  npx caracms init
28
28
  ```
29
29
 
30
- It prints every file it would add and every edit it would make, as a diff, and changes
31
- nothing. When it looks right, apply it and install what it added:
30
+ It asks where the site will run, [CaraCMS Cloud](#caracms-cloud) or self-hosted, then
31
+ prints every file it would add and every edit it would make, as a diff, and changes
32
+ nothing. (It asks only in a terminal: an agent or a script passes `--cloud` or
33
+ `--self-hosted`.) When it looks right, apply it and install what it added:
32
34
 
33
35
  ```sh
34
36
  npx caracms init --write
@@ -45,9 +47,12 @@ job of the `caracms-adopt` skill, one page at a time.
45
47
 
46
48
  Run from the site's root:
47
49
 
48
- - `caracms init [--write] [--railway]`: add caracms to an Astro site, or bring a set-up
49
- site's skills, schemas and CLAUDE.md block up to date. `--railway` also writes a
50
- Railway setup; without it, `init` prints the variables any host needs (Hosting, below).
50
+ - `caracms init [--write] [--railway] [--cloud | --self-hosted]`: add caracms to an Astro
51
+ site, or bring a set-up site's skills, schemas and CLAUDE.md block up to date.
52
+ `--cloud` or `--self-hosted` says where the site runs, written into `caracms.config.ts`
53
+ as `services` ([Where the site runs](#where-the-site-runs-services)); `--cloud` on a
54
+ self-hosted site also prints the steps of the move. `--railway` also writes a Railway
55
+ setup; without it, `init` prints the variables any host needs (Hosting, below).
51
56
  - `caracms validate [--fix] [--json]`: check the content against `caracms.config.ts`.
52
57
  Exits 1 when there are errors. `--fix` runs `caracms schemas` first, then lays each
53
58
  content file out as the editor saves it (two-space JSON; only whitespace changes).
@@ -59,8 +64,8 @@ Run from the site's root:
59
64
  keeping every key ([Moving a site's images in](#moving-a-sites-images-in)).
60
65
  - `caracms start`: serve the built site (Hosting, below).
61
66
 
62
- The skills (`caracms-content`, `caracms-blocks` and `caracms-adopt`, in `skills/`) say how
63
- the rest works, matching the installed version.
67
+ The skills (`caracms-content`, `caracms-blocks`, `caracms-adopt`, `caracms-tina` and
68
+ `caracms-hosting`, in `skills/`) say how the rest works, matching the installed version.
64
69
 
65
70
  ## Collections and addresses
66
71
 
@@ -129,7 +134,7 @@ These go on the site's server. `caracms init` prints the same list.
129
134
 
130
135
  | Name | Needed | What it's for |
131
136
  |---|---|---|
132
- | `CARACMS_CLOUD_KEY` | instead of the GitHub and image variables | The site's key from CaraCMS Cloud (`cck_…`). It replaces the token or App, `GITHUB_REPO` and the image variables: see [CaraCMS Cloud](#caracms-cloud). |
137
+ | `CARACMS_CLOUD_KEY` | instead of the GitHub, image, form and email variables | The site's key from CaraCMS Cloud (`cck_…`). It replaces the token or App, `GITHUB_REPO`, the image variables, and the form and email ones: see [CaraCMS Cloud](#caracms-cloud). |
133
138
  | `GITHUB_REPO` | required, but not with the cloud key | The site's repository, as owner/name. |
134
139
  | `CARACMS_GITHUB_TOKEN` | required, or the App, or the cloud key | A fine-grained token (`github_pat_…`) on that one repo: Contents read and write, Metadata read (Deployments read is optional). Never `GITHUB_TOKEN`. |
135
140
  | `GITHUB_APP_ID` | instead of the token | The GitHub App's ID, for anyone setting up many sites. Set the token or the App, not both. |
@@ -139,8 +144,8 @@ These go on the site's server. `caracms init` prints the same list.
139
144
  | `CARACMS_SECRET` | required | Signs the editors' sessions. A long random string. |
140
145
  | `CARACMS_CLIENT_IP_HEADER` | required off Railway, Fly and Render | Where the visitor's IP comes from: `x-real-ip` for nginx with `proxy_set_header X-Real-IP $remote_addr`, `x-forwarded-for` for Caddy, or `socket` if nothing sits in front of this server. `caracms start` won't start without it. |
141
146
  | `CARACMS_ALLOW` | for email sign-in | Who may sign in, comma-separated: addresses (`aoife@fernhill.ie`) or whole domains (`@fernhill.ie`). The client adds their own people in Site Settings. |
142
- | `RESEND_API_KEY` | for email sign-in | Sends the sign-in links (and form notifications) through Resend. |
143
- | `FORMS_FROM` | for email sign-in | The address those emails come from, on a domain verified in Resend. |
147
+ | `RESEND_API_KEY` | for email sign-in, but not with the cloud key | Sends the sign-in links (and form notifications) through Resend. |
148
+ | `FORMS_FROM` | for email sign-in, but not with the cloud key | The address those emails come from, on a domain verified in Resend. |
144
149
  | `CARACMS_BACKUP_USER` | for the backup sign-in | The backup sign-in, for when email sign-in links can't arrive. Both or neither. |
145
150
  | `CARACMS_BACKUP_PASSWORD` | for the backup sign-in | The backup sign-in's password. |
146
151
  | `CARACMS_RUNNING_COMMIT` | optional | The commit this server runs, where the host doesn't say (Railway and Render do). |
@@ -155,9 +160,9 @@ A site that uses images, video or forms needs these as well. The image variables
155
160
  | Feature | Variables |
156
161
  |---|---|
157
162
  | Images (the media bucket and imgproxy) | `CARACMS_MEDIA_URL` (imgproxy's address), `IMGPROXY_KEY`, `IMGPROXY_SALT`, `S3_BUCKET`, `S3_ENDPOINT`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`; on CaraCMS Cloud, none of them: `CARACMS_CLOUD_KEY` alone |
158
- | Video | `MUX_TOKEN_ID`, `MUX_TOKEN_SECRET` |
159
- | Form submissions (a second bucket, at the media bucket's endpoint and region) | `SUBMISSIONS_S3_BUCKET`, `SUBMISSIONS_S3_ACCESS_KEY_ID`, `SUBMISSIONS_S3_SECRET_ACCESS_KEY` |
160
- | Form emails | `RESEND_API_KEY`, `FORMS_FROM` (as above) |
163
+ | Video | `MUX_TOKEN_ID`, `MUX_TOKEN_SECRET`; on CaraCMS Cloud, none of them: `CARACMS_CLOUD_KEY` alone |
164
+ | Form submissions (a second bucket, at the media bucket's endpoint and region) | `SUBMISSIONS_S3_BUCKET`, `SUBMISSIONS_S3_ACCESS_KEY_ID`, `SUBMISSIONS_S3_SECRET_ACCESS_KEY`; on CaraCMS Cloud, none of them: `CARACMS_CLOUD_KEY` alone |
165
+ | Form emails | `RESEND_API_KEY`, `FORMS_FROM` (as above); on CaraCMS Cloud, none of them |
161
166
  | Spam checks on forms | `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY` |
162
167
  | A password on the whole site before launch | `CARACMS_PRELAUNCH` |
163
168
  | Try again through Railway's API | `CARACMS_RAILWAY_TOKEN` (Railway sets the rest) |
@@ -204,7 +209,8 @@ It prints where the IP comes from when it starts.
204
209
  - Commit: `RAILWAY_GIT_COMMIT_SHA`, which Railway sets.
205
210
  - `caracms init --railway` writes `.railway/railway.ts`, which describes the whole
206
211
  project (the site, Redis, the buckets, imgproxy). Railway's own variables cover the
207
- repo, so `GITHUB_REPO` isn't needed.
212
+ repo, so `GITHUB_REPO` isn't needed. With `--cloud` it writes only the site and Redis:
213
+ the cloud brings the rest.
208
214
  - Try again: `CARACMS_RAILWAY_TOKEN`, a project token. Railway has no deploy hook.
209
215
 
210
216
  ### Render
@@ -317,6 +323,64 @@ only, with the same permissions as the token.
317
323
 
318
324
  ### CaraCMS Cloud
319
325
 
326
+ #### Where the site runs: `services`
327
+
328
+ `caracms.config.ts` says where the site's services (GitHub access, images, forms and
329
+ email, video) come from, and nothing else decides it:
330
+
331
+ ```ts
332
+ export default defineSite({
333
+ services: 'cloud', // or 'self-hosted'
334
+ collections: { … },
335
+ …
336
+ });
337
+ ```
338
+
339
+ - **`'cloud'`**: everything comes through `CARACMS_CLOUD_KEY`. The self-hosted variables
340
+ (the GitHub token or App and `GITHUB_REPO`, the image, video, and form and email ones
341
+ below) are **ignored**, never read, and each is named once in the build log and once
342
+ when the server starts:
343
+
344
+ > S3_BUCKET is set, but this site runs on CaraCMS Cloud (caracms.config.ts): it's ignored. Remove it once the old bucket and imgproxy are retired.
345
+
346
+ So a site can move with its old variables still set, and remove them afterwards. A
347
+ production build or server with no key fails with "This site runs on CaraCMS Cloud
348
+ (caracms.config.ts), but CARACMS_CLOUD_KEY isn't set. Make a key for it in
349
+ app.caracms.com." (the build fails; a running server's `/admin` answers 503 with it, and
350
+ the public site keeps serving). It never falls back to the self-hosted variables.
351
+ `astro dev` without a key is local mode, as always.
352
+ - **`'self-hosted'`**: the site's own variables, as [the variables](#the-variables) list
353
+ them. A `CARACMS_CLOUD_KEY` beside them is ignored the same way.
354
+ - **Not set** (a site from before caracms 0.8.0): the key decides, and setting it beside
355
+ the site's own variables is refused, as each section below says. The next `caracms init`
356
+ adds the field.
357
+
358
+ `caracms init --cloud` or `--self-hosted` writes the field (as the first line inside
359
+ `defineSite({`). With neither, `init` asks in a terminal and, run by an agent or a script,
360
+ leaves a set-up site's mode unset (and says so) or stops on a new one. Once the field is
361
+ there, `init` reads it and never asks again.
362
+
363
+ **Moving a self-hosted site onto the cloud.** `caracms init --cloud` on a self-hosted site
364
+ writes the field and prints the steps of the move, in order, each with the check that it
365
+ worked: make the site, its key and the App's install in app.caracms.com; push the field
366
+ (the host's build fails until the key is set: the old deployment keeps serving); copy the
367
+ images in with [`caracms media push`](#moving-a-sites-images-in); set the key on the host;
368
+ upload the video clips again; remove every variable the cloud replaces (it names them all,
369
+ marking the ones it found in `.env` and `.railway/railway.ts`; it can't see the host's own
370
+ page, so check that too); retire what the cloud replaced; and, last, cut
371
+ `.railway/railway.ts` down to the site and Redis. `init` never edits an existing
372
+ `.railway/railway.ts`: `railway config apply` would remove the buckets with it. With a key
373
+ in the environment or `.env`, `init --cloud` also checks it, showing the site, its team,
374
+ the linked repo and whether the App is installed on it, or the cloud's reason for refusing
375
+ it (it never prints the key). The `caracms-hosting` skill walks through all of this.
376
+
377
+ **Leaving.** `caracms init --self-hosted` on a cloud site writes the field back and prints
378
+ the way back: set up the site's own services ([Hosting](#hosting)), copy the site's folder
379
+ from the cloud's media bucket to a bucket of your own (the keys in content stay the same),
380
+ set the variables, then remove the key.
381
+
382
+ #### GitHub access on CaraCMS Cloud
383
+
320
384
  A site run by CaraCMS can take its GitHub access from CaraCMS Cloud instead of a token or
321
385
  an App of its own. The cloud gives the site's server a short-lived token for the site's
322
386
  repo, and only that repo, about once an hour.
@@ -329,11 +393,14 @@ repo, and only that repo, about once an hour.
329
393
  site's server.
330
394
 
331
395
  The key replaces `CARACMS_GITHUB_TOKEN` (or `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`)
332
- and `GITHUB_REPO`: the repo comes from the cloud when the server starts, so setting
333
- `GITHUB_REPO` beside the key is refused, and so is setting a token or an App as well. It
334
- also replaces the image variables ([Images](#images-on-caracms-cloud), below).
396
+ and `GITHUB_REPO`: the repo comes from the cloud when the server starts. With `services: 'cloud'`
397
+ they're ignored; on a site whose config doesn't say, setting `GITHUB_REPO` beside the key is
398
+ refused, and so is setting a token or an App as well. It
399
+ also replaces the image variables ([Images](#images-on-caracms-cloud), below), the
400
+ video ones ([Video](#video-on-caracms-cloud)), and the form and email ones
401
+ ([Forms and email](#forms-and-email-on-caracms-cloud)).
335
402
  Everything else in [the variables](#the-variables) is still the site's own: `SITE_URL`,
336
- `REDIS_URL`, `CARACMS_SECRET`, the sign-in variables, and the video and form variables.
403
+ `REDIS_URL`, `CARACMS_SECRET`, `CARACMS_ALLOW`, the backup sign-in, and Turnstile.
337
404
  There's no webhook: a check once a minute keeps the editor's Publish status up to date.
338
405
 
339
406
  If the cloud says no when the server starts (the App isn't installed on the repo, the
@@ -348,7 +415,8 @@ start.
348
415
  With the key, the site's images live in its own folder of CaraCMS Cloud's media bucket and
349
416
  are served, resized and signed, from `img.caracms.cloud`. Set none of `S3_ENDPOINT`,
350
417
  `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `IMGPROXY_KEY`,
351
- `IMGPROXY_SALT` or `CARACMS_MEDIA_URL`: setting any of them beside the key is refused with
418
+ `IMGPROXY_SALT` or `CARACMS_MEDIA_URL`. With `services: 'cloud'` they're ignored; on a site
419
+ whose config doesn't say, setting any of them beside the key is refused with
352
420
 
353
421
  > Set CARACMS_CLOUD_KEY or the site's own media variables, not both: remove S3_BUCKET.
354
422
 
@@ -368,12 +436,46 @@ builds. A running site keeps serving whatever the cloud does.
368
436
  `caracms media import <files…>` works with the key too (from the environment or the
369
437
  site's `.env`), with the same rules and output, and its uploads count like any other.
370
438
 
439
+ #### Forms and email on CaraCMS Cloud
440
+
441
+ With the key, the site needs no submissions bucket and no Resend account. Set none of
442
+ `SUBMISSIONS_S3_BUCKET`, `SUBMISSIONS_S3_ACCESS_KEY_ID`, `SUBMISSIONS_S3_SECRET_ACCESS_KEY`,
443
+ `RESEND_API_KEY` or `FORMS_FROM`. With `services: 'cloud'` they're ignored; on a site whose
444
+ config doesn't say, setting any of them beside the key is refused, at build and when the
445
+ server starts, with
446
+
447
+ > Set CARACMS_CLOUD_KEY or the site's own forms and email variables, not both: remove RESEND_API_KEY and FORMS_FROM.
448
+
449
+ The site's server still makes every check on a submission (the fields, the honeypot, the
450
+ rate limit, Turnstile) and writes its email, as before. The cloud stores the submission
451
+ and its attachments, and sends the email once it's stored. The editor's inbox reads them
452
+ from the cloud, unchanged.
453
+
454
+ - **Form emails** come from `forms@notify.caracms.com`, named "<brand name> – <form label>"
455
+ (for example "Urban Return – Contact"), with Reply-To the visitor. They go to the form's
456
+ recipients in Site Settings, at most the first ten (the server log says when some were
457
+ left off).
458
+ - **Each site sends at most 50 form emails a day** (UTC) unless CaraCMS raises it. Past
459
+ that, a submission is still stored, and the inbox shows it as "Not emailed: this site
460
+ reached its daily email limit in CaraCMS Cloud (50). Ask CaraCMS to raise it." It isn't
461
+ sent later.
462
+ - **Sign-in links** come from `no-reply@accounts.caracms.com`, in the same words as before.
463
+
464
+ **On a laptop**, or anywhere that isn't a production server (`NODE_ENV=production`),
465
+ nothing goes to the cloud, even with the key: submissions are kept in
466
+ `.caracms/submissions/` and both emails are written to the server log, so a test enquiry
467
+ never reaches a client's inbox.
468
+
469
+ A site on an older caracms keeps its own bucket and Resend until it updates and removes
470
+ them.
471
+
371
472
  #### Moving a site's images in
372
473
 
373
474
  `caracms media push` copies everything under `uploads/` in the site's own bucket into its
374
475
  CaraCMS Cloud folder, under the same keys, so the content doesn't change. It's the one
375
- command that takes both credentials, so give the site's own `S3_*` on its command line
376
- rather than in `.env` (the running site refuses both):
476
+ command that takes both credentials. The old bucket's `S3_*` can stay in `.env` once the
477
+ config says `services: 'cloud'` (the running site ignores them); on a site whose config
478
+ doesn't say, give them on its command line instead (it refuses both):
377
479
 
378
480
  ```bash
379
481
  S3_ENDPOINT=https://t3.storageapi.dev S3_REGION=auto S3_BUCKET=… S3_ACCESS_KEY_ID=… S3_SECRET_ACCESS_KEY=… npx caracms media push
@@ -385,6 +487,30 @@ uploader, and never changes the source bucket. It prints `{ copied, skipped, fai
385
487
  A site bigger than its storage limit stops at the limit with the cloud's sentence: ask
386
488
  CaraCMS to raise it, then run it again.
387
489
 
490
+ #### Video on CaraCMS Cloud
491
+
492
+ With the key, the site's clips are uploaded to CaraCMS Cloud's Mux and listed from the
493
+ site's own clips there; the editor works exactly as it does with a Mux account of your
494
+ own. Set neither `MUX_TOKEN_ID` nor `MUX_TOKEN_SECRET`. With `services: 'cloud'` they're
495
+ ignored; on a site whose config doesn't say, setting either beside the key is refused with
496
+
497
+ > Set CARACMS_CLOUD_KEY or the site's own Mux variables, not both: remove MUX_TOKEN_ID.
498
+
499
+ Clips play on the site straight from Mux (`stream.mux.com`, `image.mux.com`), as before:
500
+ the public pages never call the cloud. The minutes a site stores count towards its video
501
+ limit (60 minutes unless CaraCMS has set another), and once it's reached, a new upload is
502
+ refused with the cloud's sentence: "This site has reached its video limit in CaraCMS
503
+ Cloud (60 minutes). Ask CaraCMS to raise it." A clip counts once Mux has finished with
504
+ it, so one upload can take a site past its limit; the next is refused.
505
+
506
+ **Moving a site's video in** means uploading its clips again in the editor and placing
507
+ them where the old ones were. There's no copy command. Until a clip is replaced, the old
508
+ one keeps playing on the site from the site's old Mux account: the page only needs its
509
+ playback ID. In the editor it shows its poster and "A clip" (it isn't in the cloud's
510
+ library), and saving other changes on the page leaves it as it is. Remove `MUX_TOKEN_ID`
511
+ and `MUX_TOKEN_SECRET` when you set the key; keep the old Mux account until every clip
512
+ has been replaced.
513
+
388
514
  #### Working locally
389
515
 
390
516
  **Anyone working on a real site, wherever they are, uses that site's CaraCMS Cloud**: your
package/bin/caracms.mjs CHANGED
@@ -31,7 +31,9 @@ import { createGate, describeClientIpSource, markProcessGated, parsePrelaunch, p
31
31
 
32
32
  const USAGE = `Usage: caracms <command>, from the site's root
33
33
 
34
- init [--write] [--railway] Add caracms to an existing Astro site. Shows the change; --write applies it.
34
+ init [--write] [--railway] [--cloud | --self-hosted]
35
+ Add caracms to an existing Astro site, and say where it runs (CaraCMS
36
+ Cloud or self-hosted). Shows the change; --write applies it.
35
37
  schemas Write schemas/ from caracms.config.ts, and add $schema to content files.
36
38
  validate [--fix] [--json] Check content against caracms.config.ts. --fix runs \`schemas\` first,
37
39
  then lays content files out as the editor saves them;
@@ -180,10 +182,16 @@ async function main(argv) {
180
182
  if (rest.length) return usage(`start takes no arguments.`);
181
183
  return start();
182
184
  case 'init': {
183
- const f = flags('init', rest, ['--write', '--railway']);
185
+ const f = flags('init', rest, ['--write', '--railway', '--cloud', '--self-hosted']);
184
186
  if (f.error) return usage(f.error);
185
187
  const { run } = await import('./init.mjs');
186
- process.exitCode = await run({ root: process.cwd(), write: f.set.has('--write'), railway: f.set.has('--railway') });
188
+ process.exitCode = await run({
189
+ root: process.cwd(),
190
+ write: f.set.has('--write'),
191
+ railway: f.set.has('--railway'),
192
+ cloud: f.set.has('--cloud'),
193
+ selfHosted: f.set.has('--self-hosted'),
194
+ });
187
195
  return;
188
196
  }
189
197
  case 'schemas':