create-astroid 0.7.1 → 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 +8 -8
- package/index.mjs +32 -32
- package/package.json +2 -2
- package/template/README.md +22 -22
- package/template/astro.config.mjs +3 -3
- package/template/docs/ARCHITECTURE.md +3 -3
- package/template/docs/DECISIONS.md +9 -9
- package/template/docs/RUNBOOK.md +5 -5
- package/template/drizzle.config.ts +4 -4
- package/template/scripts/seed-editors.mjs +2 -2
- package/template/src/auth.ts +3 -3
- package/template/src/components/LouiseEdit.astro +8 -8
- package/template/src/layouts/Site.astro +2 -2
- package/template/src/pages/api/auth/[...all].ts +1 -1
- package/template/src/pages/contact.astro +4 -4
- package/template/src/pages/index.astro +8 -8
- package/template/src/pages/login.astro +9 -9
- package/template/src/pages/robots.txt.ts +3 -3
- package/template/src/pages/sitemap.xml.ts +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# create-astroid
|
|
2
2
|
|
|
3
|
-
Scaffold a new **Astroid** site
|
|
4
|
-
Cloudflare Workers
|
|
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
|
|
18
|
-
- The generated trio
|
|
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
|
-
|
|
78
|
-
- [`louise-toolkit`](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/louise)
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
109
|
+
* - an exact version—what `pnpm pack` leaves behind for a former
|
|
110
110
|
* `workspace:*`.
|
|
111
|
-
* - an already-caretted range
|
|
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
|
|
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
|
-
//
|
|
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)
|
|
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
|
|
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
|
|
310
|
-
// shop has customers who sign in, reorder, and track orders
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
423
|
-
// editors, workspace tooling
|
|
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
|
|
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
|
|
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
|
|
457
|
-
// premise of the framework
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@louise-toolkit/astro": "^0.2.2",
|
|
35
35
|
"better-auth": "^1.7.2",
|
|
36
36
|
"louise-toolkit": "^0.31.0",
|
|
37
|
-
"astroidjs": "0.
|
|
37
|
+
"astroidjs": "0.13.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=26.0.0"
|
package/template/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# __BRAND_NAME__
|
|
2
2
|
|
|
3
|
-
An editable, multi-editor site on Cloudflare Workers
|
|
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)
|
|
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
|
|
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
|
|
28
|
+
provision them and ship—then seed content + your first editor (below).
|
|
29
29
|
|
|
30
|
-
### Zero-CLI
|
|
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
|
|
34
|
+
`wrangler.jsonc`, and deploys—no local tooling:
|
|
35
35
|
|
|
36
36
|
[](https://deploy.workers.cloudflare.com/?url=<YOUR_GITHUB_REPO_URL>)
|
|
37
37
|
|
|
38
|
-
### One command
|
|
38
|
+
### One command—`astroid deploy`
|
|
39
39
|
|
|
40
40
|
Provisions the still-placeholder bindings, applies migrations, prompts for
|
|
41
|
-
secrets, and deploys
|
|
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
|
|
83
|
-
2. The link signs you in and drops you at **`/?louise
|
|
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
|
|
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
|
|
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
|
|
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
|
|
125
|
-
| `src/schema.ts` · `src/worker.ts` · `src/middleware.ts` | **Generated
|
|
126
|
-
| `wrangler.jsonc` | Yours to edit
|
|
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
|
|
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
|
|
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
|
|
142
|
-
| `docs/RUNBOOK.md` | Operating it
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
35
|
+
- **Sections**—the shipped library as-is, fixed slots, or bespoke sections
|
|
36
36
|
injected into the catalog?
|
|
37
|
-
- **Commerce
|
|
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
|
|
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
|
|
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
|
|
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
|
|
45
|
+
- **Service worker / PWA**—a manifest is cheap; a service worker is a cache
|
|
46
46
|
invalidation problem you now own.
|
package/template/docs/RUNBOOK.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8
|
-
// they're generated by `louise
|
|
9
|
-
// NOT by Drizzle, so they're
|
|
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
|
|
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
|
|
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, "''")}'`;
|
package/template/src/auth.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
59
|
-
// ClientRouter fires on the initial load, not only on navigations
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
// editor
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
110
|
+
: "That didn't go through—please try again.";
|
|
111
111
|
} catch {
|
|
112
|
-
msg.textContent = "Something went wrong
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
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 }), {
|