create-astroid 0.7.0 → 0.7.2

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
@@ -1,7 +1,7 @@
1
1
  # create-astroid
2
2
 
3
- Scaffold a new **Astroid** site — an editable, multi-editor Astro app on
4
- Cloudflare Workers — in one command.
3
+ Scaffold a new **Astroid** site—an editable, multi-editor Astro app on
4
+ Cloudflare Workers—in one command.
5
5
 
6
6
  ```sh
7
7
  pnpm create astroid my-site
@@ -14,8 +14,8 @@ pnpm create astroid my-site
14
14
 
15
15
  A working floor, not a blank page:
16
16
 
17
- - `astroid.config.ts` — the one typed config the rest is generated from
18
- - The generated trio — `src/schema.ts`, `src/worker.ts`, `src/middleware.ts`
17
+ - `astroid.config.ts`—the one typed config the rest is generated from
18
+ - The generated trio—`src/schema.ts`, `src/worker.ts`, `src/middleware.ts`
19
19
  (Drizzle schema, editor routes in collision-free order, the shared middleware)
20
20
  - `wrangler.jsonc` with every binding stubbed and clearly marked for you to fill
21
21
  - A baseline Astro app with an **inline-editable home page**, magic-link editor
@@ -73,10 +73,10 @@ Astro → renderer / router / build
73
73
  Astroid → opinions: theme, sections, config, scaffold (astroidjs)
74
74
  ```
75
75
 
76
- - [`astroidjs`](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/astroid)
77
- — the meta-framework and the `astroid` CLI this scaffold writes a project for
78
- - [`louise-toolkit`](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/louise)
79
- — the underlying toolkit
76
+ - [`astroidjs`](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/astroid)—the
77
+ meta-framework and the `astroid` CLI this scaffold writes a project for
78
+ - [`louise-toolkit`](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/louise)—the
79
+ underlying toolkit
80
80
 
81
81
  ## License
82
82
 
package/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
3
3
  //
4
- // `create-astroid` — scaffold a new Astroid site in one command:
4
+ // `create-astroid`—scaffold a new Astroid site in one command:
5
5
  //
6
6
  // pnpm create astroid@latest my-site
7
7
  // pnpm create astroid@latest my-site --key coracle --name "Coracle Coffee" --color "#1f6f78" --host coracle.coffee
@@ -9,7 +9,7 @@
9
9
  // It writes the floor: the `defineAstroid` config, the generated
10
10
  // schema/worker/middleware trio + wrangler.jsonc (via astroidjs), the Better Auth
11
11
  // migration (via louise-toolkit/auth), and the baseline Astro app from ./template.
12
- // Binding ids are placeholders — provision them, then `astroid deploy` (or
12
+ // Binding ids are placeholders—provision them, then `astroid deploy` (or
13
13
  // wrangler) fills them in. The generators are the SAME ones `astroid generate`
14
14
  // uses, so a fresh project is already in sync.
15
15
 
@@ -37,7 +37,7 @@ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
37
37
  // Files (and dirs) whose leading `_` is stripped on copy (npm strips real
38
38
  // dotfiles from a published package, so they ship as `_gitignore` /
39
39
  // `_env.example` / `_github/…`). Applied per entry in `copyTemplate`, so a
40
- // directory renames too — `_github` becomes `.github` and its contents follow.
40
+ // directory renames too—`_github` becomes `.github` and its contents follow.
41
41
  const DOTFILE_RENAMES = {
42
42
  _gitignore: ".gitignore",
43
43
  "_env.example": ".env.example",
@@ -46,7 +46,7 @@ const DOTFILE_RENAMES = {
46
46
 
47
47
  // Archetype → default editable home sections. Imported from astroidjs rather
48
48
  // than duplicated here: as a literal in this file it could name a section that
49
- // doesn't exist and nothing would say so (it did — `marquee`, `featured`,
49
+ // doesn't exist and nothing would say so (it did—`marquee`, `featured`,
50
50
  // `story`, and `visit` had no component for months). Over there it's typed
51
51
  // against the section catalog, so a stale name fails the build. See #277.
52
52
  const ARCHETYPE_SECTIONS = ASTROID_ARCHETYPE_SECTIONS;
@@ -96,19 +96,19 @@ const slugify = (s) =>
96
96
  * every release, and when it rots the failure is silent and total: the template
97
97
  * imported `astroidjs/astro` while pinning `^0.1.0`, a range whose newest match
98
98
  * had no such export, so every scaffolded project died before Astro loaded its
99
- * config. CI could not see it — the clean-room smoke test pins both packages to
99
+ * config. CI could not see it—the clean-room smoke test pins both packages to
100
100
  * tarballs via pnpm `overrides`, which is exactly what erases these ranges.
101
101
  *
102
102
  * Three shapes reach the `declared` value, and all three have to end up as one
103
103
  * caret range:
104
104
  *
105
- * - `workspace:*` — a sibling in this repo (`astroidjs`). Falls back to the
105
+ * - `workspace:*`—a sibling in this repo (`astroidjs`). Falls back to the
106
106
  * version of the copy actually resolved on disk, which is what the scaffold
107
107
  * would install anyway. `pnpm pack` rewrites these to a concrete version, so
108
108
  * a PUBLISHED create-astroid never carries one.
109
- * - an exact version — what `pnpm pack` leaves behind for a former
109
+ * - an exact version—what `pnpm pack` leaves behind for a former
110
110
  * `workspace:*`.
111
- * - an already-caretted range — what an external dependency is written as now
111
+ * - an already-caretted range—what an external dependency is written as now
112
112
  * that `louise-toolkit` and `@louise-toolkit/astro` live in another repo.
113
113
  *
114
114
  * That last one is why `stripRange` exists. Prefixing `^` onto `^0.27.0` yields
@@ -182,7 +182,7 @@ function astroidConfigSource(config) {
182
182
  const parts = [
183
183
  'import { defineAstroid } from "astroidjs";',
184
184
  "",
185
- "// The whole shape of this site — one typed config. `astroid generate` (run by",
185
+ "// The whole shape of this site—one typed config. `astroid generate` (run by",
186
186
  "// `astroid dev`/`build`) turns it into src/schema.ts, src/worker.ts, and",
187
187
  "// src/middleware.ts; `astroid doctor` keeps them honest.",
188
188
  "export default defineAstroid({",
@@ -208,14 +208,14 @@ function astroidConfigSource(config) {
208
208
  : []),
209
209
  // Must be emitted, for the same reason the portal is: `astroid generate`
210
210
  // rebuilds the middleware and CSP from THIS file, so a config that dropped
211
- // `modules` would regenerate a project missing whatever they contribute —
212
- // for the map, a policy without `worker-src blob:`, which renders an empty
211
+ // `modules` would regenerate a project missing whatever they contribute—for
212
+ // the map, a policy without `worker-src blob:`, which renders an empty
213
213
  // canvas with no obvious cause.
214
214
  ...(config.modules?.length ? [` modules: ${JSON.stringify(config.modules)},`] : []),
215
215
  // Must be emitted: `astroid generate` rebuilds the middleware from THIS
216
216
  // file, so a config that omitted the portal would regenerate a middleware
217
217
  // with no guard while src/portal-auth.ts sat there unused. And the FULL shape
218
- // (tablePrefix, signUp) — not a bare `{ enabled: true }` — so a regenerate
218
+ // (tablePrefix, signUp)—not a bare `{ enabled: true }`—so a regenerate
219
219
  // reproduces the SAME unprefixed `user`/`session` seam the 0002_portal_auth
220
220
  // migration created, rather than defaulting the prefix back to `portal_`.
221
221
  ...(config.portal?.enabled
@@ -279,7 +279,7 @@ async function main() {
279
279
  const argv = process.argv.slice(2);
280
280
  const { flags, positionals } = parseArgs(argv);
281
281
 
282
- // Handle these before any prompting — otherwise `--help` reads as a truthy
282
+ // Handle these before any prompting—otherwise `--help` reads as a truthy
283
283
  // flag and drops the user into the interactive scaffold instead. The short
284
284
  // forms are read off argv directly: parseArgs only treats `--` as a flag, so
285
285
  // a bare `-h` would otherwise be taken as the target directory.
@@ -306,8 +306,8 @@ async function main() {
306
306
  const archetype = ARCHETYPES.includes(archetypeRaw) ? archetypeRaw : "marketing";
307
307
  const color = flags.color || (await prompt("Brand color (hex)", "#5b4bff"));
308
308
  const host = flags.host && flags.host !== true ? flags.host : undefined;
309
- // The customer PORTAL is opt-in via --portal, but a storefront IMPLIES one — a
310
- // shop has customers who sign in, reorder, and track orders — so enable it there
309
+ // The customer PORTAL is opt-in via --portal, but a storefront IMPLIES one—a
310
+ // shop has customers who sign in, reorder, and track orders—so enable it there
311
311
  // by default. (Commerce below stays opt-in: infra a marketing site shouldn't carry.)
312
312
  const portal = flags.portal === true || flags.portal === "true" || archetype === "storefront";
313
313
  // The map module is opt-in and pulls real weight (maplibre-gl is ~1 MB), so
@@ -336,7 +336,7 @@ async function main() {
336
336
  }
337
337
  // Refused, not ignored, without Square: the checkout route is scaffolded ONCE,
338
338
  // so a multi-merchant store that silently got the single-location route would
339
- // ring every sale against one ambient SQUARE_LOCATION_ID — and look fine doing it.
339
+ // ring every sale against one ambient SQUARE_LOCATION_ID—and look fine doing it.
340
340
  const squareLocationsRaw = flags["square-locations"];
341
341
  const squareLocations =
342
342
  typeof squareLocationsRaw === "string" ? squareLocationsRaw.toLowerCase() : undefined;
@@ -371,7 +371,7 @@ async function main() {
371
371
  },
372
372
  }
373
373
  : {}),
374
- // Unprefixed `user`/`session` (customers — email + password), so the studio's
374
+ // Unprefixed `user`/`session` (customers—email + password), so the studio's
375
375
  // `louise_`-prefixed tables and the portal's never collide (mirrors the
376
376
  // reference storefront). `signUp: true` because a shop lets customers register.
377
377
  ...(portal ? { portal: { enabled: true, tablePrefix: "", signUp: true } } : {}),
@@ -385,7 +385,7 @@ async function main() {
385
385
  const siteUrl = host ? `https://${host}` : `https://${key}.workers.dev`;
386
386
  const envBindings = generateAstroidEnvBindings(config);
387
387
  const portalLocals = generateAstroidPortalLocals(config);
388
- // The realtime DO namespace, or nothing — same rule as the queue bindings: a
388
+ // The realtime DO namespace, or nothing—same rule as the queue bindings: a
389
389
  // declaration is a promise, so never type a binding wrangler.jsonc won't create.
390
390
  const realtimeEnv = generateAstroidRealtimeEnv(config);
391
391
  // The Square Web Payments public vars, or nothing.
@@ -397,17 +397,17 @@ async function main() {
397
397
  ARCHETYPE: archetype,
398
398
  SITE_URL: siteUrl,
399
399
  // Extra CloudflareEnv members the queue pipeline needs, or nothing. A
400
- // declaration is a promise — a marketing site must not claim a binding its
400
+ // declaration is a promise—a marketing site must not claim a binding its
401
401
  // wrangler.jsonc never creates.
402
402
  ASTROID_ENV_BINDINGS: [envBindings, realtimeEnv, checkoutEnv].filter(Boolean).join("\n")
403
403
  ? `\n${[envBindings, realtimeEnv, checkoutEnv].filter(Boolean).join("\n")}`
404
404
  : "",
405
- // The portal session on App.Locals, or nothing — a project that types a
405
+ // The portal session on App.Locals, or nothing—a project that types a
406
406
  // local it never sets invites a null-check nobody needs.
407
407
  ASTROID_PORTAL_LOCALS: portalLocals ? `\n${portalLocals}` : "",
408
408
  // Placeholder-seeded secrets for whichever modules this project enabled, so
409
- // a fresh clone has a COMPLETE binding set that all reads as unconfigured —
410
- // every module takes its dormant path deliberately rather than tripping over
409
+ // a fresh clone has a COMPLETE binding set that all reads as unconfigured—every
410
+ // module takes its dormant path deliberately rather than tripping over
411
411
  // an undefined binding. Empty for a project with no credentialed module.
412
412
  ASTROID_MODULE_SECRETS: generateAstroidSecretsEnv(config),
413
413
  };
@@ -419,11 +419,11 @@ async function main() {
419
419
  //
420
420
  // Merged by PARSING the file rather than substituting a token into it:
421
421
  // a `__TOKEN__` inside a JSON object makes template/package.json invalid
422
- // JSON, and everything that scans a repo for manifests — Snyk, Dependabot,
423
- // editors, workspace tooling — parses it and fails. (It did.)
422
+ // JSON, and everything that scans a repo for manifests—Snyk, Dependabot,
423
+ // editors, workspace tooling—parses it and fails. (It did.)
424
424
  //
425
425
  // The `astroidjs` / `louise-toolkit` ranges are DERIVED (see
426
- // `toolkitRanges`), never taken from template/package.json — a hand-written
426
+ // `toolkitRanges`), never taken from template/package.json—a hand-written
427
427
  // range there silently rots into a scaffold that can't build. The literals
428
428
  // it still carries are placeholders that keep the file valid JSON.
429
429
  //
@@ -446,15 +446,15 @@ async function main() {
446
446
  for (const file of generateAstroidProject(config)) write(dir, file.path, file.contents);
447
447
  write(dir, "wrangler.jsonc", generateAstroidWrangler(config));
448
448
 
449
- // 3b. Every scaffold-once module file this config implies — the queue seam and
449
+ // 3b. Every scaffold-once module file this config implies—the queue seam and
450
450
  // webhook receivers, the portfolio gallery page, the PWA service worker +
451
451
  // manifest + headers, the map tile route + embed, the portal's second auth
452
452
  // instance and its mounted catch-all.
453
453
  //
454
454
  // ONE list, imported from astroidjs, because `astroid generate` writes the
455
455
  // same files when a config gains a module after scaffold. Hand-listing them
456
- // here was the only way to produce them, so editing the config — the entire
457
- // premise of the framework — regenerated a trio importing `./queue.js` and
456
+ // here was the only way to produce them, so editing the config—the entire
457
+ // premise of the framework—regenerated a trio importing `./queue.js` and
458
458
  // `./portal-auth.js` that nothing had written, and `astroid doctor` called
459
459
  // it healthy. Sharing the list is what keeps the two paths honest.
460
460
  for (const file of generateAstroidScaffoldFiles(config)) {
@@ -471,7 +471,7 @@ async function main() {
471
471
  write(dir, file.path, file.contents);
472
472
  }
473
473
 
474
- // 4. The Better Auth migration (louise-toolkit) — auth tables are fenced out of
474
+ // 4. The Better Auth migration (louise-toolkit)—auth tables are fenced out of
475
475
  // drizzle-kit, so they're generated rather than diffed from schema.ts. Loaded
476
476
  // dynamically: it pulls in `better-auth` (an optional peer), which may not be
477
477
  // resolvable at scaffold time. If not, leave a stub + a one-liner to generate
@@ -479,12 +479,12 @@ async function main() {
479
479
  let authMigrationOk = false;
480
480
  try {
481
481
  const { generateAuthSchemaSql } = await import("louise-toolkit/auth");
482
- // The EDITOR instance's tables — `louise_`-prefixed (the editor convention),
482
+ // The EDITOR instance's tables—`louise_`-prefixed (the editor convention),
483
483
  // leaving the unprefixed `user`/`session` names free for a second/portal
484
484
  // instance. Must match the `tablePrefix` in src/auth.ts and the `louise_user`
485
485
  // table the generated `editorsRoute` reads.
486
486
  write(dir, "migrations/0001_auth.sql", generateAuthSchemaSql({ tablePrefix: "louise_" }));
487
- // The portal's own auth tables — a SECOND Better Auth instance sharing one D1
487
+ // The portal's own auth tables—a SECOND Better Auth instance sharing one D1
488
488
  // but never a row, so a portal account can't sign into the studio and an
489
489
  // editor doesn't appear in the portal. `customers: true` (email + password)
490
490
  // and the config's tablePrefix, so this schema matches the scaffolded
@@ -506,7 +506,7 @@ async function main() {
506
506
  );
507
507
  // Same stub for the portal's prefixed set. Without it a portal scaffold
508
508
  // looks complete, builds, and fails on the first sign-in with a missing
509
- // table — the one failure mode a stub exists to prevent.
509
+ // table—the one failure mode a stub exists to prevent.
510
510
  if (config.portal?.enabled) {
511
511
  write(
512
512
  dir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-astroid",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Scaffold a new Astroid site — an editable, multi-editor Astro app on Cloudflare Workers — in one command.",
5
5
  "keywords": [
6
6
  "astro",
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.2",
34
- "@louise-toolkit/astro": "^0.2.1",
34
+ "@louise-toolkit/astro": "^0.2.2",
35
35
  "better-auth": "^1.7.2",
36
- "louise-toolkit": "^0.30.1",
37
- "astroidjs": "0.12.0"
36
+ "louise-toolkit": "^0.31.0",
37
+ "astroidjs": "0.13.0"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=26.0.0"
@@ -1,11 +1,11 @@
1
1
  # __BRAND_NAME__
2
2
 
3
- An editable, multi-editor site on Cloudflare Workers — scaffolded with
3
+ An editable, multi-editor site on Cloudflare Workers—scaffolded with
4
4
  [Astroid](https://github.com/bowenlabs/louise-toolkit) (Astro + Louise Toolkit).
5
5
 
6
6
  The whole shape of this site lives in one typed config, [`astroid.config.ts`](./astroid.config.ts).
7
7
  `src/schema.ts`, `src/worker.ts`, and `src/middleware.ts` are **generated** from it
8
- (they carry a "do not hand-edit" banner) — run `pnpm generate` after any config
8
+ (they carry a "do not hand-edit" banner)—run `pnpm generate` after any config
9
9
  change, or just use `pnpm dev`/`pnpm build`, which regenerate first.
10
10
 
11
11
  ## Develop
@@ -19,26 +19,26 @@ pnpm dev # astroid dev: regenerate, then astro dev
19
19
  > **Previewing the built worker?** `pnpm dev` (astro dev) serves on localhost, so
20
20
  > an empty `SESSION_SECRET` is fine there. A local `wrangler dev` against the
21
21
  > built `dist/` output routes the request through your `hosts` domain instead of
22
- > localhost, so the editor routes need a real `SESSION_SECRET` in `.dev.vars` —
23
- > otherwise sign-in 500s with "SESSION_SECRET is not configured".
22
+ > localhost, so the editor routes need a real `SESSION_SECRET` in `.dev.vars`—
23
+ > otherwise sign-in returns 500 with "SESSION_SECRET is not configured".
24
24
 
25
25
  ## Deploy
26
26
 
27
27
  Astroid wrote `wrangler.jsonc` with placeholder binding ids. Pick a path to
28
- provision them and ship — then seed content + your first editor (below).
28
+ provision them and ship—then seed content + your first editor (below).
29
29
 
30
- ### Zero-CLI — Deploy to Cloudflare
30
+ ### Zero-CLI—Deploy to Cloudflare
31
31
 
32
32
  Push this repo to GitHub and drop this button in place (swap in your repo URL).
33
33
  Cloudflare clones the repo, provisions the D1/R2/KV bindings declared in
34
- `wrangler.jsonc`, and deploys — no local tooling:
34
+ `wrangler.jsonc`, and deploys—no local tooling:
35
35
 
36
36
  [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=<YOUR_GITHUB_REPO_URL>)
37
37
 
38
- ### One command — `astroid deploy`
38
+ ### One command—`astroid deploy`
39
39
 
40
40
  Provisions the still-placeholder bindings, applies migrations, prompts for
41
- secrets, and deploys — through your local `wrangler`:
41
+ secrets, and deploys—through your local `wrangler`:
42
42
 
43
43
  ```sh
44
44
  pnpm astroid deploy --dry-run # preview the exact commands it will run
@@ -79,10 +79,10 @@ There are no passwords and no editor list in env to keep in sync.
79
79
 
80
80
  1. Go to **`/login`** and enter a seeded editor's email. In local dev there's no
81
81
  email binding, so the magic link is printed to the `wrangler`/`astro dev`
82
- console — open it from there. In production it's emailed.
83
- 2. The link signs you in and drops you at **`/?louise`** — edit mode. The **edit
82
+ console—open it from there. In production it's emailed.
83
+ 2. The link signs you in and drops you at **`/?louise`**—edit mode. The **edit
84
84
  bar** appears with **Settings** and **Done**.
85
- 3. The home page's **title and body are editable in place** — click into them and
85
+ 3. The home page's **title and body are editable in place**—click into them and
86
86
  type. Edits stage a **draft**; **Publish** (in the edit bar) promotes it live.
87
87
  **Settings** opens the drawer: **Pages** (create/edit other pages), **Media**,
88
88
  **Settings** (brand, nav, contact, SEO), and **Users** (invite/remove editors).
@@ -107,9 +107,9 @@ wrangler deploy # or: pnpm astroid deploy
107
107
 
108
108
  Storefront sites (`--commerce square`) are scaffolded with a server-authoritative
109
109
  payment route at `src/pages/api/checkout.ts` and a `<SquareCard>` input. **Keep
110
- its sequence** — extend it (shipping, tax, an order row, a receipt), don't replace
110
+ its sequence**—extend it (shipping, tax, an order row, a receipt), don't replace
111
111
  it. It re-prices every line from the D1 catalog mirror (the client's price is a
112
- _staleness check_, never an input to the charge — accept a `unitPrice` from the
112
+ _staleness check_, never an input to the charge—accept a `unitPrice` from the
113
113
  request body and anyone buys anything for a penny) and derives the Square
114
114
  idempotency key from the verified cart **and** the cart id. Hand-rolling
115
115
  `createPayment` without a stable, cart-scoped idempotency key is the failure this
@@ -121,27 +121,27 @@ single charge.
121
121
 
122
122
  | Path | What |
123
123
  | --- | --- |
124
- | `astroid.config.ts` | The one typed config — brand, archetype, sections. |
125
- | `src/schema.ts` · `src/worker.ts` · `src/middleware.ts` | **Generated** — don't hand-edit. |
126
- | `wrangler.jsonc` | Yours to edit — real binding ids, routes, secrets. |
124
+ | `astroid.config.ts` | The one typed config—brand, archetype, sections. |
125
+ | `src/schema.ts` · `src/worker.ts` · `src/middleware.ts` | **Generated**—don't hand-edit. |
126
+ | `wrangler.jsonc` | Yours to edit—real binding ids, routes, secrets. |
127
127
  | `src/auth.ts` | The editor auth seam (Better Auth, DB-managed editors). |
128
128
  | `src/pages/` · `src/components/` · `src/layouts/` | Your Astro app. |
129
129
  | `migrations/` | `0000_content.sql` (content + FTS) · `0001_auth.sql` (Better Auth). |
130
130
  | `scripts/seed-editors.mjs` | Bootstrap the first editor. |
131
- | `docs/` | ARCHITECTURE · RUNBOOK · DECISIONS — stubs to fill in as you go. |
131
+ | `docs/` | ARCHITECTURE · RUNBOOK · DECISIONS—stubs to fill in as you go. |
132
132
 
133
133
  ## The docs/ trio
134
134
 
135
135
  Three near-empty documents, scaffolded on purpose. Three production Astroid sites
136
136
  each wrote the same three without coordinating, and converged on the same
137
- headings — so you inherit the questions rather than a blank directory.
137
+ headings—so you inherit the questions rather than a blank directory.
138
138
 
139
139
  | | What goes in it |
140
140
  | --- | --- |
141
- | `docs/ARCHITECTURE.md` | How this site is put together — request flow, content model, bindings. |
142
- | `docs/RUNBOOK.md` | Operating it — local dev, migrations, secrets, deploy, and **common breakages**. |
141
+ | `docs/ARCHITECTURE.md` | How this site is put together—request flow, content model, bindings. |
142
+ | `docs/RUNBOOK.md` | Operating it—local dev, migrations, secrets, deploy, and **common breakages**. |
143
143
  | `docs/DECISIONS.md` | Choices the framework leaves open, and why you made yours. |
144
144
 
145
145
  `DECISIONS.md` ships with a list of the questions every Astroid site has to
146
- answer — editors, rich-text storage, sections, commerce, migrations, CSP, edge
146
+ answer—editors, rich-text storage, sections, commerce, migrations, CSP, edge
147
147
  caching. Delete each one as it becomes a real entry.
@@ -22,14 +22,14 @@ export default defineConfig({
22
22
  build: { ...ASTROID_VITE_BUILD },
23
23
  },
24
24
  // Route caching (ADR 0004). This provider is what turns `Astro.cache.set(...)`
25
- // into a `Cloudflare-CDN-Cache-Control` header — which the generated worker's
25
+ // into a `Cloudflare-CDN-Cache-Control` header—which the generated worker's
26
26
  // `withEdgeCache` layer reads as its "store this" signal and then STRIPS, so
27
27
  // Cloudflare's own cookie-blind edge cache never sees it.
28
28
  //
29
29
  // Opt-in per response: a route that never calls `Astro.cache.set` (or calls
30
30
  // `set(false)`, as an edit-mode render does) goes out `no-store`. Nothing
31
31
  // personalized is ever cached. Published pages opt in from index.astro, gated
32
- // on the ASTROID_EDGE_CACHE var — which is "false" until you have walked the
32
+ // on the ASTROID_EDGE_CACHE var—which is "false" until you have walked the
33
33
  // activation runbook on a preview deploy.
34
34
  cache: { provider: cacheCloudflare() },
35
35
  // Content-Security-Policy, composed by Astroid from your config: it derives the
@@ -41,7 +41,7 @@ export default defineConfig({
41
41
  // 'unsafe-inline' and a hash in that directive would void it.
42
42
  //
43
43
  // This is why the inline scripts here (login.astro, LouiseEdit.astro) avoid
44
- // is:inline/define:vars — those can't be hashed and would be blocked. Need
44
+ // is:inline/define:vars—those can't be hashed and would be blocked. Need
45
45
  // another origin? Add it to `security.cspOrigins` in astroid.config.ts.
46
46
  security: astroidSecurity(astroidConfig),
47
47
  });
@@ -4,7 +4,7 @@ How __BRAND_NAME__ is put together: what happens to a request, where content
4
4
  lives, and which pieces are the framework's rather than yours.
5
5
 
6
6
  > Scaffolded by `create-astroid`. Describe what is TRUE of this site, not what
7
- > Astroid does in general — that is documented at
7
+ > Astroid does in general—that is documented at
8
8
  > <https://docs.astroidjs.org>. The useful content here is the part that would
9
9
  > surprise someone who knows the framework.
10
10
 
@@ -25,14 +25,14 @@ request → middleware (session, edit mode, rate limits)
25
25
 
26
26
  <!-- The collections this site defines and what each one is for. Note which
27
27
  fields are rich text, which are structured, and anything a section depends
28
- on being present. The schema is generated from astroid.config.ts — link to
28
+ on being present. The schema is generated from astroid.config.ts—link to
29
29
  it rather than restating it, and record the REASONING here. -->
30
30
 
31
31
  ## Editing
32
32
 
33
33
  <!-- Which parts of a page are editable and how they are marked. Sections vs
34
34
  inline fields vs settings. If a section is deliberately not editable, that
35
- is worth a line — the next person will assume it was an oversight. -->
35
+ is worth a line—the next person will assume it was an oversight. -->
36
36
 
37
37
  ## Auth
38
38
 
@@ -6,7 +6,7 @@ Choices __BRAND_NAME__ made that the framework deliberately leaves open, and why
6
6
  > decision's *reasoning* is the part nobody can reconstruct later. Record the
7
7
  > choice when you make it, while the alternatives are still fresh.
8
8
  >
9
- > Not an ADR log — no status field, no numbering ceremony. One heading per
9
+ > Not an ADR log—no status field, no numbering ceremony. One heading per
10
10
  > decision, newest at the top, and a line about what you did instead.
11
11
 
12
12
  ## Template
@@ -28,19 +28,19 @@ have cost. If there wasn't one, say so; "no real alternative" is a finding.
28
28
  Delete each one as it becomes a real entry above. Every Astroid site meets these,
29
29
  and the framework takes no position on any of them:
30
30
 
31
- - **Editors** — DB-managed rows, or an environment allowlist? One auth instance
31
+ - **Editors**—DB-managed rows, or an environment allowlist? One auth instance
32
32
  or two, if customers sign in as well?
33
- - **Rich text storage** — HTML or document JSON? This is very hard to change
33
+ - **Rich text storage**—HTML or document JSON? This is very hard to change
34
34
  later; the sites that picked HTML did it for portability and said so.
35
- - **Sections** — the shipped library as-is, fixed slots, or bespoke sections
35
+ - **Sections**—the shipped library as-is, fixed slots, or bespoke sections
36
36
  injected into the catalog?
37
- - **Commerce** — none, or which provider? If there is a catalog: live reads, or a
37
+ - **Commerce**—none, or which provider? If there is a catalog: live reads, or a
38
38
  D1 mirror? Where money is calculated, and what makes that server-authoritative.
39
- - **Migrations** — hand-authored SQL is the default. If you adopt a generator,
39
+ - **Migrations**—hand-authored SQL is the default. If you adopt a generator,
40
40
  write down what happens to the existing hand-authored files.
41
- - **Content security policy** — generated, or hand-maintained in
41
+ - **Content security policy**—generated, or hand-maintained in
42
42
  `astro.config.mjs`? A section that needs an inline style forces this question.
43
- - **Edge caching** — off by default. Turning it on has a revalidation story that
43
+ - **Edge caching**—off by default. Turning it on has a revalidation story that
44
44
  belongs here, not in a commit message.
45
- - **Service worker / PWA** — a manifest is cheap; a service worker is a cache
45
+ - **Service worker / PWA**—a manifest is cheap; a service worker is a cache
46
46
  invalidation problem you now own.
@@ -4,7 +4,7 @@ Operating __BRAND_NAME__: local dev, migrations, secrets, deploy, and what to do
4
4
  when something breaks.
5
5
 
6
6
  > Scaffolded by `create-astroid`. The headings are the ones three production
7
- > Astroid sites arrived at independently — fill them in as you go. A heading with
7
+ > Astroid sites arrived at independently—fill them in as you go. A heading with
8
8
  > nothing under it is a question you have not had to answer yet, which is useful
9
9
  > information on its own.
10
10
 
@@ -23,7 +23,7 @@ pnpm exec wrangler d1 migrations apply DB --local
23
23
  OWNER_EMAIL=you@example.com pnpm seed:editors
24
24
  ```
25
25
 
26
- Then open `/louise` and request the magic link — in local dev it is **printed to
26
+ Then open `/louise` and request the magic link—in local dev it is **printed to
27
27
  the dev console**, since there is no email binding. Follow it to `/?louise` for
28
28
  edit mode.
29
29
 
@@ -49,7 +49,7 @@ wrangler deploy
49
49
  ```
50
50
 
51
51
  <!-- Who deploys, from where, and what gates it? If deploys are automatic on push
52
- to main, say so here — that is the first thing a new person asks. -->
52
+ to main, say so here—that is the first thing a new person asks. -->
53
53
 
54
54
  ## D1 migrations
55
55
 
@@ -82,13 +82,13 @@ wrangler secret put SESSION_SECRET
82
82
  ## Editing the live site
83
83
 
84
84
  <!-- Who the editors are and how they are added. Astroid ships DB-managed editors
85
- by default — `pnpm seed:editors` writes the first one — rather than an env
85
+ by default—`pnpm seed:editors` writes the first one—rather than an env
86
86
  allowlist. If you changed that, this is where it is written down. -->
87
87
 
88
88
  ## Common breakages
89
89
 
90
90
  <!-- The section that pays for the whole document. Add an entry every time
91
- something surprises you, with the symptom FIRST — that is what someone
91
+ something surprises you, with the symptom FIRST—that is what someone
92
92
  searches for at the time.
93
93
 
94
94
  Format that works:
@@ -4,10 +4,10 @@ import { defineConfig } from "drizzle-kit";
4
4
  // by `wrangler d1 migrations apply DB`. The schema is the astroid-generated
5
5
  // content tables (louise-toolkit/db column sets).
6
6
  //
7
- // Better Auth owns its own tables (user/session/account/verification/passkey) —
8
- // they're generated by `louise gen-auth-schema` (see migrations/0001_auth.sql),
9
- // NOT by Drizzle, so they're fenced out here to keep drizzle-kit from trying to
10
- // drop or recreate them.
7
+ // Better Auth owns its own tables
8
+ // (user/session/account/verification/passkey)—they're generated by `louise
9
+ // gen-auth-schema` (see migrations/0001_auth.sql), NOT by Drizzle, so they're
10
+ // fenced out here to keep drizzle-kit from trying to drop or recreate them.
11
11
  export default defineConfig({
12
12
  dialect: "sqlite",
13
13
  schema: "./src/schema.ts",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Seed the first editor(s) — an admin `louise_user` row per OWNER_EMAIL / ENGINEER_EMAIL.
2
+ // Seed the first editors—an admin `louise_user` row per OWNER_EMAIL / ENGINEER_EMAIL.
3
3
  // A row here IS an editor and IS the magic-link allowlist, so this bootstraps
4
4
  // access before anyone can sign in. Idempotent (INSERT OR IGNORE on unique email).
5
5
  // After this, add more editors from the Users panel (never by editing env).
@@ -37,7 +37,7 @@ const now = new Date().toISOString();
37
37
  /**
38
38
  * Quote a value as a SQL string literal, doubling any embedded single quote.
39
39
  * `wrangler d1 execute` takes raw SQL via `--command` with no parameter binding,
40
- * so values have to be escaped rather than bound — and an apostrophe is legal in
40
+ * so values have to be escaped rather than bound—and an apostrophe is legal in
41
41
  * an email local part, so this is a correctness fix as much as a safety one.
42
42
  */
43
43
  const q = (value) => `'${String(value).replace(/'/g, "''")}'`;
@@ -4,7 +4,7 @@
4
4
  // DB-managed editors: an admin `louise_user` row IS an editor. The editor
5
5
  // instance's tables are `louise_`-prefixed (the editor convention), leaving the
6
6
  // unprefixed `user`/`session` names free for a second/portal instance. That
7
- // table is BOTH the role source and the magic-link allowlist — `resolveAdmins`
7
+ // table is BOTH the role source and the magic-link allowlist—`resolveAdmins`
8
8
  // reads it, so only an existing editor's email can request a sign-in link. Seed
9
9
  // the first editor with `pnpm seed:editors`; add more from the Users panel
10
10
  // (editorsRoute), never by editing env. Magic-link + passkey come from
@@ -37,7 +37,7 @@ async function resolveAdmins(): Promise<string[]> {
37
37
  return results.map((r) => r.email);
38
38
  }
39
39
 
40
- /** The branded sign-in email — Astroid's template over your mail theme. */
40
+ /** The branded sign-in email—Astroid's template over your mail theme. */
41
41
  function renderMagicLinkEmail({ url, toEmail }: { url: string; toEmail: string }): MagicLinkEmail {
42
42
  return magicLinkEmail(MAIL_THEME, { url, toEmail });
43
43
  }
@@ -57,7 +57,7 @@ function getAuth(request: Request): Promise<LouiseAuth> {
57
57
 
58
58
  /**
59
59
  * Re-derive the editor session from the signed Better Auth session on every
60
- * request — the seam the generated worker.ts + middleware.ts call. Null when the
60
+ * request—the seam the generated worker.ts + middleware.ts call. Null when the
61
61
  * caller isn't a signed-in editor, which is what denies edit/write access.
62
62
  */
63
63
  export async function resolveEditor(request: Request): Promise<EditorSession | null> {
@@ -1,5 +1,5 @@
1
1
  ---
2
- // Boots the in-page Louise editor — the edit bar + the Settings drawer (Pages,
2
+ // Boots the in-page Louise editor—the edit bar + the Settings drawer (Pages,
3
3
  // Media, Settings, Users). Drop it before </body> in your layout.
4
4
  //
5
5
  // Gated on `Astro.locals.editMode`, which the generated middleware sets for a
@@ -25,7 +25,7 @@ const sectionsJson = Array.isArray(sections) ? JSON.stringify(sections) : undefi
25
25
 
26
26
  // Is the live multi-editor session on? Read from the config rather than baked in
27
27
  // at scaffold time, so adding `realtime` to `modules` later takes effect here
28
- // too — `astroid generate` regenerates the worker and scaffolds the Durable
28
+ // too—`astroid generate` regenerates the worker and scaffolds the Durable
29
29
  // Object, but this file is yours and is never rewritten.
30
30
  const realtimeOn = (astroidConfig.modules ?? []).includes("realtime");
31
31
  ---
@@ -36,7 +36,7 @@ const realtimeOn = (astroidConfig.modules ?? []).includes("realtime");
36
36
  {/* Per-render values (editor name, versioned page id) ride as data-* on this
37
37
  marker element rather than a define:vars inline script. define:vars forces
38
38
  is:inline, whose content varies per request and so can't be hashed by
39
- Astro's security.csp — it would be CSP-blocked. The boot script below
39
+ Astro's security.csp—it would be CSP-blocked. The boot script below
40
40
  reads them from here; being static, it hashes cleanly into script-src.
41
41
  The element also gates the editor: no marker (a non-edit page swapped in
42
42
  by a view transition) → boot() no-ops. */}
@@ -53,10 +53,10 @@ const realtimeOn = (astroidConfig.modules ?? []).includes("realtime");
53
53
  // the edit bar; `mountSettings` renders the drawer (Pages/Media/Settings +
54
54
  // Users, backed by editorsRoute). When the page passes `versionedPageId`,
55
55
  // the bar gains Save draft + Publish, and this page's `<Editable>` fields
56
- // are edited in place — their changes stage a draft, promoted on Publish.
56
+ // are edited in place—their changes stage a draft, promoted on Publish.
57
57
  // The sections editor currently mounted on this <body>, and which boot
58
- // owns it. `boot()` runs at parse AND on `astro:page-load` — which Astro's
59
- // ClientRouter fires on the initial load, not only on navigations — so two
58
+ // owns it. `boot()` runs at parse AND on `astro:page-load`—which Astro's
59
+ // ClientRouter fires on the initial load, not only on navigations—so two
60
60
  // boots race, and the second lands while the first one's dynamic import is
61
61
  // still in flight. Without a guard both mount, and the page gets two
62
62
  // on-canvas chromes, two toolbars and two Publish buttons over one store.
@@ -65,7 +65,7 @@ const realtimeOn = (astroidConfig.modules ?? []).includes("realtime");
65
65
  // not, and it is also the one that needs disposing on a view-transition
66
66
  // nav so its flush and unsaved-changes listeners don't outlive the page.
67
67
  // Annotated, not inferred: `= null` alone infers `null`, and `astro check`
68
- // runs strict — the scaffold smoke fails on ts(7034)/ts(7005) before a
68
+ // runs strict—the scaffold smoke fails on ts(7034)/ts(7005) before a
69
69
  // scaffolded site ever builds.
70
70
  let teardown: (() => void) | null = null;
71
71
  let generation = 0;
@@ -82,7 +82,7 @@ const realtimeOn = (astroidConfig.modules ?? []).includes("realtime");
82
82
  import("astroidjs/components/sections"),
83
83
  ])
84
84
  .then(([client, settings, lib]) => {
85
- // Superseded while importing — a later boot owns the page now.
85
+ // Superseded while importing—a later boot owns the page now.
86
86
  if (gen !== generation) return;
87
87
  settings.mountSettings({
88
88
  userName: el.dataset.louiseUser || "Editor",
@@ -53,7 +53,7 @@ try {
53
53
  FROM site_settings WHERE id = 1`,
54
54
  ).first<Settings>();
55
55
  } catch {
56
- // No DB binding yet — fall through to the config defaults below.
56
+ // No DB binding yet—fall through to the config defaults below.
57
57
  }
58
58
 
59
59
  const settings = {
@@ -99,7 +99,7 @@ const settings = {
99
99
  <LouiseEdit versionedPageId={versionedPageId} sections={sections} />
100
100
  {
101
101
  /* Real-visitor Core Web Vitals. A static file from public/, so it is
102
- same-origin and already covered by `script-src 'self'` — an inline
102
+ same-origin and already covered by `script-src 'self'`—an inline
103
103
  script with generated content couldn't be hashed into the CSP.
104
104
  `defer` keeps it off the critical path; it reports once per page on
105
105
  visibilitychange via sendBeacon. Skipped in edit mode: an editor's
@@ -1,4 +1,4 @@
1
- // Better Auth catch-all — magic-link sign-in, passkey, session. The allowlist
1
+ // Better Auth catch-all—magic-link sign-in, passkey, session. The allowlist
2
2
  // gate (handleAuth → handleAuthRequest) rejects magic-link requests from
3
3
  // non-editor emails before Better Auth runs, enumeration-safe.
4
4
  import type { APIRoute } from "astro";
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  // The public contact form.
3
3
  //
4
- // The `contact` section — and the scaffolded hero/cta defaults — link here, so
4
+ // The `contact` section—and the scaffolded hero/cta defaults—link here, so
5
5
  // without this page a fresh site ships a dead link. It posts to the generated
6
6
  // worker's `formRoute`, mounted at /api/louise/forms/inquiries: same-origin POST
7
7
  // only, a honeypot field plus a minimum time-on-page, a KV rate limit, then
8
8
  // validation against the inquiries form before the row is written. Submissions
9
9
  // land in the Inquiries tab of the editor drawer.
10
10
  //
11
- // Yours to edit — the copy, the fields, the layout. (If you remove the `contact`
11
+ // Yours to edit—the copy, the fields, the layout. (If you remove the `contact`
12
12
  // section from astroid.config.ts, the form route stops being mounted and this
13
13
  // page's POST will 404; delete the page too, or keep the section.)
14
14
  import Site from "../layouts/Site.astro";
@@ -76,7 +76,7 @@ const renderedAt = Date.now();
76
76
 
77
77
  <script>
78
78
  // Progressive enhancement: without JS the form still submits (the route accepts
79
- // form-encoded bodies) — the visitor just lands on the JSON response. A
79
+ // form-encoded bodies)—the visitor just lands on the JSON response. A
80
80
  // processed <script> (no is:inline) is bundled to a same-origin file, which is
81
81
  // what keeps it inside the CSP's `script-src 'self'`.
82
82
  const form = document.querySelector<HTMLFormElement>("#contact-form");
@@ -98,7 +98,7 @@ const renderedAt = Date.now();
98
98
 
99
99
  if (response.ok) {
100
100
  form.reset();
101
- if (status) status.textContent = "Thanks — we've got it, and we'll be in touch.";
101
+ if (status) status.textContent = "Thanks—we've got it, and we'll be in touch.";
102
102
  return;
103
103
  }
104
104
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- // The home page — an editable `pages` row rendered in place. Reads the published
2
+ // The home page—an editable `pages` row rendered in place. Reads the published
3
3
  // `home` row for the public view; in edit mode it reads the latest DRAFT so an
4
4
  // editor's in-progress edits resume. The <Editable> fields carry the markers the
5
5
  // Louise client turns into inline editors; saves stage a draft (versionedPageId),
@@ -17,7 +17,7 @@ const { editMode } = Astro.locals;
17
17
  // Edge caching (ADR 0004). Opting in emits `Cloudflare-CDN-Cache-Control`, which
18
18
  // the worker's `withEdgeCache` layer reads as "store this", then strips.
19
19
  //
20
- // Both conditions matter. Edit mode must NEVER opt in — a cached editor render
20
+ // Both conditions matter. Edit mode must NEVER opt in—a cached editor render
21
21
  // would carry draft content and the inline-edit hooks into the shared public
22
22
  // entry. And the var is off by default because `caches.default` survives
23
23
  // Cloudflare Dev Mode and Purge Everything; walk the activation runbook in
@@ -33,8 +33,8 @@ if (editMode || env.ASTROID_EDGE_CACHE !== "true") {
33
33
  Astro.cache.set({ maxAge: 60 });
34
34
  }
35
35
 
36
- // `seo_*` are the page's SEO overrides, deliberately separate from `title` —
37
- // which is the on-page H1. The home page passes NO title to <Site>, so <title>
36
+ // `seo_*` are the page's SEO overrides, deliberately separate from `title`—which
37
+ // is the on-page H1. The home page passes NO title to <Site>, so <title>
38
38
  // is the site-wide default ("Acme Coffee", not "Acme Coffee | Acme Coffee");
39
39
  // set `seo_title` on the row to override just the head.
40
40
  interface HomeRow {
@@ -54,12 +54,12 @@ try {
54
54
  "SELECT id, title, body, sections, seo_title, seo_description, og_image, noindex FROM pages WHERE slug = 'home'",
55
55
  ).first<HomeRow>();
56
56
  } catch {
57
- // No DB binding yet (pre-provision) — fall through to the seed-me prompt.
57
+ // No DB binding yet (pre-provision)—fall through to the seed-me prompt.
58
58
  }
59
59
 
60
60
  let title = page?.title ?? "__BRAND_NAME__";
61
61
  let body = page?.body ?? "";
62
- // `sections` is a JSON string in D1. Bad JSON is a render-time non-event — the
62
+ // `sections` is a JSON string in D1. Bad JSON is a render-time non-event—the
63
63
  // page falls back to its prose body rather than 500ing on one malformed row.
64
64
  function parseSections(raw: string | null | undefined): unknown[] {
65
65
  if (!raw) return [];
@@ -89,13 +89,13 @@ if (editMode && page) {
89
89
  if (typeof d.title === "string") title = d.title;
90
90
  if (typeof d.body === "string") body = d.body;
91
91
  // Sections stage as drafts like any other field, so edit mode must render
92
- // the draft's array — otherwise an editor's in-progress section edits
92
+ // the draft's array—otherwise an editor's in-progress section edits
93
93
  // vanish on reload while their title edits survive.
94
94
  if (Array.isArray(d.sections)) sections = d.sections;
95
95
  else if (typeof d.sections === "string") sections = parseSections(d.sections);
96
96
  }
97
97
  } catch {
98
- // Non-fatal — fall back to the live row.
98
+ // Non-fatal—fall back to the live row.
99
99
  }
100
100
  }
101
101
  ---
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  // Editor sign-in. Emails a magic link (Better Auth) to an allowlisted editor;
3
3
  // clicking it starts a session and lands on `/?louise` (edit mode on). The gate
4
- // is enumeration-safe — the response is identical whether or not the email is an
5
- // editor — so the UI always says "check your email".
4
+ // is enumeration-safe—the response is identical whether or not the email is an
5
+ // editor—so the UI always says "check your email".
6
6
  import { env } from "cloudflare:workers";
7
7
  import { turnstileSiteKey } from "louise-toolkit/auth";
8
8
  import Site from "../layouts/Site.astro";
@@ -14,8 +14,8 @@ export const prerender = false;
14
14
  // This has to match the server exactly. `getLouiseAuth` registers Better Auth's
15
15
  // captcha plugin on /sign-in/magic-link the moment BOTH halves of the pair are
16
16
  // real, and that plugin rejects any request without an `x-captcha-response`
17
- // header. So a page that never rendered a widget meant provisioning Turnstile —
18
- // which .env.example openly invites you to do — locked the owner out of their
17
+ // header. So a page that never rendered a widget meant provisioning Turnstile—which
18
+ // .env.example openly invites you to do—locked the owner out of their
19
19
  // own sign-in, with a 403 and no clue why.
20
20
  //
21
21
  // `turnstileSiteKey` returns null for the always-passing TEST key, which is the
@@ -46,7 +46,7 @@ const siteKey = turnstileSiteKey(env);
46
46
  {
47
47
  siteKey && (
48
48
  /* Rendered explicitly by the script below, which reads the key
49
- from here — a processed script can't take `define:vars`. */
49
+ from here—a processed script can't take `define:vars`. */
50
50
  <div id="captcha" data-sitekey={siteKey} />
51
51
  )
52
52
  }
@@ -68,7 +68,7 @@ const siteKey = turnstileSiteKey(env);
68
68
  const msg = document.getElementById("msg");
69
69
  const captcha = document.getElementById("captcha");
70
70
 
71
- // Absent when no real site key is set — then the server's captcha plugin is
71
+ // Absent when no real site key is set—then the server's captcha plugin is
72
72
  // off too, and the request goes without a token.
73
73
  let widget: TurnstileWidget | null = null;
74
74
  const ready = captcha?.dataset.sitekey
@@ -79,7 +79,7 @@ const siteKey = turnstileSiteKey(env);
79
79
  .catch(() => {
80
80
  // The server will refuse a request without a token, so say so now
81
81
  // rather than after the owner has typed their email.
82
- if (msg) msg.textContent = "The sign-in check didn't load — reload the page to try again.";
82
+ if (msg) msg.textContent = "The sign-in check didn't load—reload the page to try again.";
83
83
  })
84
84
  : Promise.resolve();
85
85
 
@@ -107,9 +107,9 @@ const siteKey = turnstileSiteKey(env);
107
107
  // editor, so a failure here is the captcha, a rate limit, or an outage.
108
108
  msg.textContent = res.ok
109
109
  ? "If that email is an editor, a sign-in link is on its way."
110
- : "That didn't go through — please try again.";
110
+ : "That didn't go through—please try again.";
111
111
  } catch {
112
- msg.textContent = "Something went wrong — please try again.";
112
+ msg.textContent = "Something went wrong—please try again.";
113
113
  } finally {
114
114
  // A token is single-use, and this one is spent whatever happened. Without
115
115
  // a fresh one, "send it again" posts the spent token and is refused.
@@ -1,4 +1,4 @@
1
- // robots.txt — origin-aware, with the disallow list derived from your Astroid
1
+ // robots.txt—origin-aware, with the disallow list derived from your Astroid
2
2
  // config (the editor + its API always; portal and checkout routes when those
3
3
  // modules are on). Scaffolded once and yours to edit: add a Disallow here when
4
4
  // you add a route crawlers shouldn't reach.
@@ -10,7 +10,7 @@ import astroidConfig from "../../astroid.config.js";
10
10
  export const prerender = false;
11
11
 
12
12
  export const GET: APIRoute = async (context) => {
13
- // The origin actually serving this request — never a configured domain. A
13
+ // The origin actually serving this request—never a configured domain. A
14
14
  // preview deploy that advertises the production host invites its content to
15
15
  // be indexed under the real domain.
16
16
  const origin = new URL(context.request.url).origin;
@@ -24,7 +24,7 @@ export const GET: APIRoute = async (context) => {
24
24
  }>();
25
25
  disableIndexing = Boolean(row?.disable_indexing);
26
26
  } catch {
27
- // No DB binding yet (pre-provision) — fall through to the crawlable default.
27
+ // No DB binding yet (pre-provision)—fall through to the crawlable default.
28
28
  }
29
29
 
30
30
  return new Response(astroidRobotsTxt(astroidConfig, { origin, disableIndexing }), {
@@ -1,4 +1,4 @@
1
- // sitemap.xml — the published `pages` rows, plus whatever else this site serves.
1
+ // sitemap.xml—the published `pages` rows, plus whatever else this site serves.
2
2
  // Scaffolded once and yours to edit: add your own routes (a product catalog, a
3
3
  // gallery) to the `entries` array below.
4
4
  //
@@ -20,7 +20,7 @@ export const GET: APIRoute = async (context) => {
20
20
  "SELECT slug, updated_at FROM pages WHERE status = 'published' AND noindex = 0",
21
21
  ).all<{ slug: string; updated_at: number | null }>();
22
22
  for (const row of results ?? []) {
23
- // `home` is served at "/", which is already listed — never at "/home".
23
+ // `home` is served at "/", which is already listed—never at "/home".
24
24
  if (row.slug === "home") continue;
25
25
  // Drizzle stores `updated_at` as a Unix timestamp in SECONDS; <lastmod>
26
26
  // wants a W3C datetime, so a raw epoch number would be invalid.
@@ -28,7 +28,7 @@ export const GET: APIRoute = async (context) => {
28
28
  entries.push({ path: `/${row.slug}`, lastmod });
29
29
  }
30
30
  } catch {
31
- // No DB binding yet (pre-provision) — ship the root-only sitemap.
31
+ // No DB binding yet (pre-provision)—ship the root-only sitemap.
32
32
  }
33
33
 
34
34
  return new Response(astroidSitemapXml(astroidConfig, entries, { origin }), {