create-astroid 0.7.1 → 0.7.3

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
 
@@ -23,6 +23,7 @@ import {
23
23
  ASTROID_MAP_DEPENDENCIES,
24
24
  defineAstroid,
25
25
  generateAstroidEnvBindings,
26
+ generateAstroidHomeSeed,
26
27
  generateAstroidPortalLocals,
27
28
  generateAstroidProject,
28
29
  generateAstroidCheckoutEnv,
@@ -37,7 +38,7 @@ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
37
38
  // Files (and dirs) whose leading `_` is stripped on copy (npm strips real
38
39
  // dotfiles from a published package, so they ship as `_gitignore` /
39
40
  // `_env.example` / `_github/…`). Applied per entry in `copyTemplate`, so a
40
- // directory renames too — `_github` becomes `.github` and its contents follow.
41
+ // directory renames too—`_github` becomes `.github` and its contents follow.
41
42
  const DOTFILE_RENAMES = {
42
43
  _gitignore: ".gitignore",
43
44
  "_env.example": ".env.example",
@@ -46,7 +47,7 @@ const DOTFILE_RENAMES = {
46
47
 
47
48
  // Archetype → default editable home sections. Imported from astroidjs rather
48
49
  // 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`,
50
+ // doesn't exist and nothing would say so (it did—`marquee`, `featured`,
50
51
  // `story`, and `visit` had no component for months). Over there it's typed
51
52
  // against the section catalog, so a stale name fails the build. See #277.
52
53
  const ARCHETYPE_SECTIONS = ASTROID_ARCHETYPE_SECTIONS;
@@ -96,19 +97,19 @@ const slugify = (s) =>
96
97
  * every release, and when it rots the failure is silent and total: the template
97
98
  * imported `astroidjs/astro` while pinning `^0.1.0`, a range whose newest match
98
99
  * 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
100
+ * config. CI could not see it—the clean-room smoke test pins both packages to
100
101
  * tarballs via pnpm `overrides`, which is exactly what erases these ranges.
101
102
  *
102
103
  * Three shapes reach the `declared` value, and all three have to end up as one
103
104
  * caret range:
104
105
  *
105
- * - `workspace:*` — a sibling in this repo (`astroidjs`). Falls back to the
106
+ * - `workspace:*`—a sibling in this repo (`astroidjs`). Falls back to the
106
107
  * version of the copy actually resolved on disk, which is what the scaffold
107
108
  * would install anyway. `pnpm pack` rewrites these to a concrete version, so
108
109
  * a PUBLISHED create-astroid never carries one.
109
- * - an exact version — what `pnpm pack` leaves behind for a former
110
+ * - an exact version—what `pnpm pack` leaves behind for a former
110
111
  * `workspace:*`.
111
- * - an already-caretted range — what an external dependency is written as now
112
+ * - an already-caretted range—what an external dependency is written as now
112
113
  * that `louise-toolkit` and `@louise-toolkit/astro` live in another repo.
113
114
  *
114
115
  * That last one is why `stripRange` exists. Prefixing `^` onto `^0.27.0` yields
@@ -182,7 +183,7 @@ function astroidConfigSource(config) {
182
183
  const parts = [
183
184
  'import { defineAstroid } from "astroidjs";',
184
185
  "",
185
- "// The whole shape of this site — one typed config. `astroid generate` (run by",
186
+ "// The whole shape of this site—one typed config. `astroid generate` (run by",
186
187
  "// `astroid dev`/`build`) turns it into src/schema.ts, src/worker.ts, and",
187
188
  "// src/middleware.ts; `astroid doctor` keeps them honest.",
188
189
  "export default defineAstroid({",
@@ -208,14 +209,14 @@ function astroidConfigSource(config) {
208
209
  : []),
209
210
  // Must be emitted, for the same reason the portal is: `astroid generate`
210
211
  // 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
212
+ // `modules` would regenerate a project missing whatever they contribute—for
213
+ // the map, a policy without `worker-src blob:`, which renders an empty
213
214
  // canvas with no obvious cause.
214
215
  ...(config.modules?.length ? [` modules: ${JSON.stringify(config.modules)},`] : []),
215
216
  // Must be emitted: `astroid generate` rebuilds the middleware from THIS
216
217
  // file, so a config that omitted the portal would regenerate a middleware
217
218
  // 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
219
+ // (tablePrefix, signUp)—not a bare `{ enabled: true }`—so a regenerate
219
220
  // reproduces the SAME unprefixed `user`/`session` seam the 0002_portal_auth
220
221
  // migration created, rather than defaulting the prefix back to `portal_`.
221
222
  ...(config.portal?.enabled
@@ -279,7 +280,7 @@ async function main() {
279
280
  const argv = process.argv.slice(2);
280
281
  const { flags, positionals } = parseArgs(argv);
281
282
 
282
- // Handle these before any prompting — otherwise `--help` reads as a truthy
283
+ // Handle these before any prompting—otherwise `--help` reads as a truthy
283
284
  // flag and drops the user into the interactive scaffold instead. The short
284
285
  // forms are read off argv directly: parseArgs only treats `--` as a flag, so
285
286
  // a bare `-h` would otherwise be taken as the target directory.
@@ -306,8 +307,8 @@ async function main() {
306
307
  const archetype = ARCHETYPES.includes(archetypeRaw) ? archetypeRaw : "marketing";
307
308
  const color = flags.color || (await prompt("Brand color (hex)", "#5b4bff"));
308
309
  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
310
+ // The customer PORTAL is opt-in via --portal, but a storefront IMPLIES one—a
311
+ // shop has customers who sign in, reorder, and track orders—so enable it there
311
312
  // by default. (Commerce below stays opt-in: infra a marketing site shouldn't carry.)
312
313
  const portal = flags.portal === true || flags.portal === "true" || archetype === "storefront";
313
314
  // The map module is opt-in and pulls real weight (maplibre-gl is ~1 MB), so
@@ -336,7 +337,7 @@ async function main() {
336
337
  }
337
338
  // Refused, not ignored, without Square: the checkout route is scaffolded ONCE,
338
339
  // 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.
340
+ // ring every sale against one ambient SQUARE_LOCATION_ID—and look fine doing it.
340
341
  const squareLocationsRaw = flags["square-locations"];
341
342
  const squareLocations =
342
343
  typeof squareLocationsRaw === "string" ? squareLocationsRaw.toLowerCase() : undefined;
@@ -371,7 +372,7 @@ async function main() {
371
372
  },
372
373
  }
373
374
  : {}),
374
- // Unprefixed `user`/`session` (customers — email + password), so the studio's
375
+ // Unprefixed `user`/`session` (customers—email + password), so the studio's
375
376
  // `louise_`-prefixed tables and the portal's never collide (mirrors the
376
377
  // reference storefront). `signUp: true` because a shop lets customers register.
377
378
  ...(portal ? { portal: { enabled: true, tablePrefix: "", signUp: true } } : {}),
@@ -385,7 +386,7 @@ async function main() {
385
386
  const siteUrl = host ? `https://${host}` : `https://${key}.workers.dev`;
386
387
  const envBindings = generateAstroidEnvBindings(config);
387
388
  const portalLocals = generateAstroidPortalLocals(config);
388
- // The realtime DO namespace, or nothing — same rule as the queue bindings: a
389
+ // The realtime DO namespace, or nothing—same rule as the queue bindings: a
389
390
  // declaration is a promise, so never type a binding wrangler.jsonc won't create.
390
391
  const realtimeEnv = generateAstroidRealtimeEnv(config);
391
392
  // The Square Web Payments public vars, or nothing.
@@ -397,17 +398,17 @@ async function main() {
397
398
  ARCHETYPE: archetype,
398
399
  SITE_URL: siteUrl,
399
400
  // Extra CloudflareEnv members the queue pipeline needs, or nothing. A
400
- // declaration is a promise — a marketing site must not claim a binding its
401
+ // declaration is a promise—a marketing site must not claim a binding its
401
402
  // wrangler.jsonc never creates.
402
403
  ASTROID_ENV_BINDINGS: [envBindings, realtimeEnv, checkoutEnv].filter(Boolean).join("\n")
403
404
  ? `\n${[envBindings, realtimeEnv, checkoutEnv].filter(Boolean).join("\n")}`
404
405
  : "",
405
- // The portal session on App.Locals, or nothing — a project that types a
406
+ // The portal session on App.Locals, or nothing—a project that types a
406
407
  // local it never sets invites a null-check nobody needs.
407
408
  ASTROID_PORTAL_LOCALS: portalLocals ? `\n${portalLocals}` : "",
408
409
  // 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
410
+ // a fresh clone has a COMPLETE binding set that all reads as unconfigured—every
411
+ // module takes its dormant path deliberately rather than tripping over
411
412
  // an undefined binding. Empty for a project with no credentialed module.
412
413
  ASTROID_MODULE_SECRETS: generateAstroidSecretsEnv(config),
413
414
  };
@@ -419,11 +420,11 @@ async function main() {
419
420
  //
420
421
  // Merged by PARSING the file rather than substituting a token into it:
421
422
  // 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.)
423
+ // JSON, and everything that scans a repo for manifests—Snyk, Dependabot,
424
+ // editors, workspace tooling—parses it and fails. (It did.)
424
425
  //
425
426
  // The `astroidjs` / `louise-toolkit` ranges are DERIVED (see
426
- // `toolkitRanges`), never taken from template/package.json — a hand-written
427
+ // `toolkitRanges`), never taken from template/package.json—a hand-written
427
428
  // range there silently rots into a scaffold that can't build. The literals
428
429
  // it still carries are placeholders that keep the file valid JSON.
429
430
  //
@@ -446,15 +447,20 @@ async function main() {
446
447
  for (const file of generateAstroidProject(config)) write(dir, file.path, file.contents);
447
448
  write(dir, "wrangler.jsonc", generateAstroidWrangler(config));
448
449
 
449
- // 3b. Every scaffold-once module file this config implies — the queue seam and
450
+ // 3a. The home page seed, built from the config's own `sections`. It used to be
451
+ // a fixed template file that seeded the marketing sections for every
452
+ // archetype, and token substitution can't escape a brand name for SQL.
453
+ write(dir, "seed/home.seed.sql", generateAstroidHomeSeed(config));
454
+
455
+ // 3b. Every scaffold-once module file this config implies—the queue seam and
450
456
  // webhook receivers, the portfolio gallery page, the PWA service worker +
451
457
  // manifest + headers, the map tile route + embed, the portal's second auth
452
458
  // instance and its mounted catch-all.
453
459
  //
454
460
  // ONE list, imported from astroidjs, because `astroid generate` writes the
455
461
  // 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
462
+ // here was the only way to produce them, so editing the config—the entire
463
+ // premise of the framework—regenerated a trio importing `./queue.js` and
458
464
  // `./portal-auth.js` that nothing had written, and `astroid doctor` called
459
465
  // it healthy. Sharing the list is what keeps the two paths honest.
460
466
  for (const file of generateAstroidScaffoldFiles(config)) {
@@ -471,7 +477,7 @@ async function main() {
471
477
  write(dir, file.path, file.contents);
472
478
  }
473
479
 
474
- // 4. The Better Auth migration (louise-toolkit) — auth tables are fenced out of
480
+ // 4. The Better Auth migration (louise-toolkit)—auth tables are fenced out of
475
481
  // drizzle-kit, so they're generated rather than diffed from schema.ts. Loaded
476
482
  // dynamically: it pulls in `better-auth` (an optional peer), which may not be
477
483
  // resolvable at scaffold time. If not, leave a stub + a one-liner to generate
@@ -479,12 +485,12 @@ async function main() {
479
485
  let authMigrationOk = false;
480
486
  try {
481
487
  const { generateAuthSchemaSql } = await import("louise-toolkit/auth");
482
- // The EDITOR instance's tables — `louise_`-prefixed (the editor convention),
488
+ // The EDITOR instance's tables—`louise_`-prefixed (the editor convention),
483
489
  // leaving the unprefixed `user`/`session` names free for a second/portal
484
490
  // instance. Must match the `tablePrefix` in src/auth.ts and the `louise_user`
485
491
  // table the generated `editorsRoute` reads.
486
492
  write(dir, "migrations/0001_auth.sql", generateAuthSchemaSql({ tablePrefix: "louise_" }));
487
- // The portal's own auth tables — a SECOND Better Auth instance sharing one D1
493
+ // The portal's own auth tables—a SECOND Better Auth instance sharing one D1
488
494
  // but never a row, so a portal account can't sign into the studio and an
489
495
  // editor doesn't appear in the portal. `customers: true` (email + password)
490
496
  // and the config's tablePrefix, so this schema matches the scaffolded
@@ -506,7 +512,7 @@ async function main() {
506
512
  );
507
513
  // Same stub for the portal's prefixed set. Without it a portal scaffold
508
514
  // looks complete, builds, and fails on the first sign-in with a missing
509
- // table — the one failure mode a stub exists to prevent.
515
+ // table—the one failure mode a stub exists to prevent.
510
516
  if (config.portal?.enabled) {
511
517
  write(
512
518
  dir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-astroid",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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.2",
34
+ "@louise-toolkit/astro": "^0.2.3",
35
35
  "better-auth": "^1.7.2",
36
- "louise-toolkit": "^0.31.0",
37
- "astroidjs": "0.12.1"
36
+ "louise-toolkit": "^0.31.1",
37
+ "astroidjs": "0.14.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 }), {
@@ -1,61 +0,0 @@
1
- -- Seed the editable home page (slug `home`) that src/pages/index.astro renders.
2
- -- Idempotent. Apply once after migrations:
3
- -- wrangler d1 execute DB --file seed/home.seed.sql --remote
4
- -- (drop --remote for the local dev D1)
5
- --
6
- -- `sections` is the page-builder array — the same shape the on-canvas editor
7
- -- reads and the server validates against the section catalog on write. Each item
8
- -- is `{"_type": …, …fields, "_settings": {…}}`, and `_settings` holds TOKENS
9
- -- (colorway/align), never CSS, so a re-theme needs no content change.
10
- --
11
- -- Seeding it is deliberate: an empty array renders an empty page, and a blank
12
- -- canvas is a worse first run than having something real to click on and edit.
13
- --
14
- -- Full-text search (the editor's Pages search) is kept in sync on PUBLISH, so a
15
- -- row inserted with raw SQL like this is NOT in the index until you either
16
- -- publish an edit to it or backfill once with `POST /api/louise/pages/reindex`
17
- -- (signed in). The page renders fine either way — only in-editor search is
18
- -- affected.
19
- --
20
- -- To override just the <head> title/description (not the on-page H1), add
21
- -- `seo_title` / `seo_description` columns below. `seo_title` is run through the
22
- -- config's title template, which already appends the brand — so set the page
23
- -- part only ("Pricing"), not "Pricing | __BRAND_NAME__".
24
- INSERT OR IGNORE INTO pages (slug, title, body, sections, status, sort_order, created_at, updated_at)
25
- VALUES (
26
- 'home',
27
- '__BRAND_NAME__',
28
- '<p>Welcome to __BRAND_NAME__. Sign in at <code>/login</code>, switch on edit mode, and change this text in place — then hit Publish.</p>',
29
- json_array(
30
- json_object(
31
- '_type', 'hero',
32
- 'heading', '__BRAND_NAME__',
33
- 'subheading', 'Edit this in place — sign in, switch on edit mode, and type.',
34
- 'ctaLabel', 'Get in touch',
35
- 'ctaHref', '/contact',
36
- '_settings', json_object('colorway', 'base', 'align', 'center')
37
- ),
38
- json_object(
39
- '_type', 'featureGrid',
40
- 'heading', 'What we do',
41
- 'items', json_array(
42
- json_object('title', 'First thing', 'body', 'Describe it here.'),
43
- json_object('title', 'Second thing', 'body', 'And this one.'),
44
- json_object('title', 'Third thing', 'body', 'And this one too.')
45
- ),
46
- '_settings', json_object('colorway', 'base')
47
- ),
48
- json_object(
49
- '_type', 'cta',
50
- 'heading', 'Ready when you are',
51
- 'body', 'Swap this copy for your own.',
52
- 'ctaLabel', 'Contact us',
53
- 'ctaHref', '/contact',
54
- '_settings', json_object('colorway', 'brand', 'align', 'center')
55
- )
56
- ),
57
- 'published',
58
- 0,
59
- unixepoch(),
60
- unixepoch()
61
- );