astroidjs 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +42 -42
  2. package/bin/astroid.mjs +35 -29
  3. package/dist/analytics/index.d.ts +3 -3
  4. package/dist/analytics/index.js +11 -11
  5. package/dist/astro/csp.d.ts +5 -5
  6. package/dist/astro/csp.js +6 -6
  7. package/dist/astro/index.js +1 -1
  8. package/dist/auth/index.d.ts +2 -2
  9. package/dist/auth/index.js +5 -5
  10. package/dist/commerce/adapters.d.ts +6 -6
  11. package/dist/commerce/adapters.js +9 -9
  12. package/dist/commerce/checkout-scaffold.d.ts +5 -5
  13. package/dist/commerce/checkout-scaffold.js +27 -27
  14. package/dist/commerce/checkout.d.ts +12 -12
  15. package/dist/commerce/checkout.js +7 -7
  16. package/dist/commerce/loader.d.ts +2 -2
  17. package/dist/commerce/loader.js +3 -3
  18. package/dist/commerce/mirror.d.ts +4 -4
  19. package/dist/commerce/mirror.js +12 -12
  20. package/dist/commerce/roles.d.ts +10 -10
  21. package/dist/commerce/roles.js +13 -13
  22. package/dist/commerce/secrets.d.ts +9 -9
  23. package/dist/commerce/secrets.js +9 -9
  24. package/dist/commerce/sync.d.ts +7 -7
  25. package/dist/commerce/sync.js +5 -5
  26. package/dist/components/sections.d.ts +9 -9
  27. package/dist/components/sections.js +12 -12
  28. package/dist/config.d.ts +89 -62
  29. package/dist/config.js +18 -18
  30. package/dist/email/inquiry.d.ts +2 -2
  31. package/dist/email/inquiry.js +1 -1
  32. package/dist/email/send.d.ts +4 -4
  33. package/dist/email/send.js +7 -7
  34. package/dist/email/templates.js +3 -3
  35. package/dist/email/theme.d.ts +1 -1
  36. package/dist/email/theme.js +4 -4
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/map/pmtiles.d.ts +5 -5
  41. package/dist/map/pmtiles.js +5 -5
  42. package/dist/map/scaffold.d.ts +2 -2
  43. package/dist/map/scaffold.js +8 -8
  44. package/dist/map/style.d.ts +4 -4
  45. package/dist/map/style.js +1 -1
  46. package/dist/portal/config.d.ts +3 -3
  47. package/dist/portal/config.js +11 -10
  48. package/dist/portal/guard.d.ts +4 -4
  49. package/dist/portal/guard.js +4 -4
  50. package/dist/portal/nav.js +2 -2
  51. package/dist/portal/scaffold.d.ts +4 -4
  52. package/dist/portal/scaffold.js +13 -13
  53. package/dist/portal/session.d.ts +11 -5
  54. package/dist/portal/session.js +10 -5
  55. package/dist/portfolio/scaffold.d.ts +1 -1
  56. package/dist/portfolio/scaffold.js +8 -8
  57. package/dist/project/actions.d.ts +1 -1
  58. package/dist/project/actions.js +18 -12
  59. package/dist/project/generate.d.ts +4 -4
  60. package/dist/project/generate.js +26 -26
  61. package/dist/project/index.d.ts +1 -0
  62. package/dist/project/index.js +2 -1
  63. package/dist/project/scaffold.d.ts +2 -2
  64. package/dist/project/scaffold.js +69 -13
  65. package/dist/project/seed.d.ts +22 -0
  66. package/dist/project/seed.js +214 -0
  67. package/dist/pwa/generate.d.ts +11 -11
  68. package/dist/pwa/generate.js +19 -19
  69. package/dist/queues/consumer.d.ts +3 -3
  70. package/dist/queues/consumer.js +2 -2
  71. package/dist/queues/messages.d.ts +4 -4
  72. package/dist/queues/messages.js +2 -2
  73. package/dist/queues/scaffold.d.ts +4 -4
  74. package/dist/queues/scaffold.js +17 -17
  75. package/dist/queues/webhook.d.ts +5 -5
  76. package/dist/queues/webhook.js +3 -3
  77. package/dist/realtime/scaffold.d.ts +4 -4
  78. package/dist/realtime/scaffold.js +12 -12
  79. package/dist/schema/collections.d.ts +42 -11
  80. package/dist/schema/collections.js +43 -42
  81. package/dist/schema/framework.d.ts +1 -1
  82. package/dist/schema/framework.js +2 -2
  83. package/dist/schema/generate.js +6 -6
  84. package/dist/schema/index.js +1 -1
  85. package/dist/secrets.d.ts +6 -6
  86. package/dist/secrets.js +6 -6
  87. package/dist/security/csp-origins.d.ts +1 -1
  88. package/dist/security/csp-origins.js +3 -3
  89. package/dist/security/rate-rules.d.ts +2 -2
  90. package/dist/security/rate-rules.js +8 -8
  91. package/dist/seo/resolve.d.ts +5 -5
  92. package/dist/seo/resolve.js +2 -2
  93. package/dist/seo/routes.d.ts +5 -5
  94. package/dist/seo/routes.js +3 -3
  95. package/dist/seo/structured-data.d.ts +6 -6
  96. package/dist/seo/structured-data.js +7 -7
  97. package/dist/status.d.ts +5 -5
  98. package/dist/status.js +7 -7
  99. package/dist/tenancy/index.d.ts +3 -3
  100. package/dist/tenancy/index.js +11 -11
  101. package/dist/worker/generate.d.ts +2 -2
  102. package/dist/worker/generate.js +91 -57
  103. package/dist/worker/index.js +1 -1
  104. package/dist/worker/routes.js +11 -11
  105. package/dist/workflow/advance.d.ts +3 -3
  106. package/dist/workflow/advance.js +6 -6
  107. package/dist/workflow/config.d.ts +4 -4
  108. package/dist/workflow/config.js +4 -4
  109. package/dist/workflow/generate.d.ts +2 -2
  110. package/dist/workflow/generate.js +11 -11
  111. package/package.json +3 -4
  112. package/src/components/Collection.tsx +5 -5
  113. package/src/components/Editable.astro +9 -9
  114. package/src/components/JustifiedGallery.astro +8 -8
  115. package/src/components/MediaSlot.astro +12 -12
  116. package/src/components/PortalShell.astro +4 -4
  117. package/src/components/RegisterSW.astro +3 -3
  118. package/src/components/Section.astro +8 -8
  119. package/src/components/Sections.astro +6 -6
  120. package/src/components/Seo.astro +3 -3
  121. package/src/components/StageBar.astro +3 -3
  122. package/src/components/StructuredData.astro +2 -2
  123. package/src/components/justify.ts +9 -9
  124. package/src/components/media-meta.ts +10 -10
  125. package/src/components/sections/AboutIntro.astro +1 -1
  126. package/src/components/sections/Contact.astro +1 -1
  127. package/src/components/sections/Cta.astro +1 -1
  128. package/src/components/sections/Faq.astro +1 -1
  129. package/src/components/sections/FeatureGrid.astro +2 -2
  130. package/src/components/sections/Hero.astro +1 -1
  131. package/src/components/sections/PricingTiers.astro +1 -1
  132. package/src/components/sections/ProductGrid.astro +1 -1
  133. package/src/components/sections/SplitImage.astro +1 -1
  134. package/src/components/sections/Steps.astro +1 -1
  135. package/src/components/sections/Testimonial.astro +1 -1
  136. package/src/components/sections.ts +17 -17
@@ -9,8 +9,8 @@
9
9
  // src/worker.ts → import { handleQueueMessage } from "./queue.js"
10
10
  // src/middleware.ts → import { resolvePortalUser } from "./portal-auth.js"
11
11
  //
12
- // The files behind those imports were written in exactly one place —
13
- // `create-astroid`'s CLI — and nothing else could produce them. So turning a
12
+ // The files behind those imports were written in exactly one
13
+ // place—`create-astroid`'s CLI—and nothing else could produce them. So turning a
14
14
  // module on AFTER scaffold, by editing the one typed config the framework is
15
15
  // built around, regenerated a trio importing files that did not exist. `astroid
16
16
  // doctor` reported "healthy" and the project failed in Vite.
@@ -37,11 +37,56 @@ import { generateAstroidEditSession } from "../realtime/scaffold.js";
37
37
  import { generatePwaHeaders, generateServiceWorker, generateWebManifest, resolvePwa, } from "../pwa/generate.js";
38
38
  import { astroidUsesQueues } from "../queues/messages.js";
39
39
  import { generateAstroidQueueSeam, generateAstroidWebhookRoutes } from "../queues/scaffold.js";
40
+ /** `src/pages-hooks.ts`—the site's pages-route hooks, scaffolded once. */
41
+ function generateAstroidPagesHooks() {
42
+ return [
43
+ "// The pages route's site-owned hooks. The generated worker passes",
44
+ "// `pagesHooks` to astroidPagesWriteHooks, which runs your `transform` before",
45
+ "// Astroid's own section sanitize and validate. Scaffolded once and yours to",
46
+ "// edit.",
47
+ "//",
48
+ "// `transform` gets the allowlisted fields of a write and returns what to",
49
+ "// store: normalize the slug, clamp a title, fill a new page's defaults.",
50
+ "// `validate` rejects a write by throwing a LouiseValidationError (a 422).",
51
+ "// `reservedSlugs` adds to the paths no page may take (ASTROID_RESERVED_SLUGS),",
52
+ "// such as one of your own file routes.",
53
+ 'import type { AstroidPagesHooks } from "astroidjs";',
54
+ "",
55
+ "export const pagesHooks: AstroidPagesHooks = {",
56
+ " reservedSlugs: [],",
57
+ "};",
58
+ "",
59
+ ].join("\n");
60
+ }
61
+ /** `src/settings-hooks.ts`—the site's settings sanitizers, scaffolded once. */
62
+ function generateAstroidSettingsHooks() {
63
+ return [
64
+ "// The Settings panel's sanitize + read hooks. The generated worker spreads",
65
+ "// `settingsHooks` into its settingsRoute call, and src/actions/index.ts",
66
+ "// passes `sanitize` to the settings Action, so both write paths clean a value",
67
+ "// the same way. Scaffolded once and yours to edit.",
68
+ "//",
69
+ "// `sanitize` maps a settings key to a function that returns the value to",
70
+ "// store. It runs before the link-scheme and media-URL checks, and only on",
71
+ "// keys the allowlist (settings.columns + settings.customKeys) already accepts.",
72
+ "// `read` transforms the merged settings on GET, for example to fill keys an",
73
+ "// older row lacks from your defaults.",
74
+ 'import type { SettingsRouteHooks } from "louise-toolkit/editor";',
75
+ "",
76
+ "export const settingsHooks: SettingsRouteHooks = {",
77
+ " sanitize: {",
78
+ " // For example, trim and cap a headline:",
79
+ ' // heroHeadline: (v) => (typeof v === "string" ? v.trim().slice(0, 120) : ""),',
80
+ " },",
81
+ "};",
82
+ "",
83
+ ].join("\n");
84
+ }
40
85
  /**
41
86
  * Every scaffold-once file this config implies.
42
87
  *
43
88
  * Ordered by module so a `generate` that writes several prints them in a stable
44
- * sequence. Returns `[]` for a plain marketing site with no modules — the
89
+ * sequence. Returns `[]` for a plain marketing site with no modules—the
45
90
  * baseline floor is entirely the regenerated trio plus the static template.
46
91
  */
47
92
  export function generateAstroidScaffoldFiles(config) {
@@ -56,7 +101,7 @@ export function generateAstroidScaffoldFiles(config) {
56
101
  files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql });
57
102
  // --- the CWV beacon -------------------------------------------------------
58
103
  // A static file under public/, so it is same-origin and covered by
59
- // `script-src 'self'` — an inline script carrying generated content could not
104
+ // `script-src 'self'`—an inline script carrying generated content could not
60
105
  // be hashed into the CSP and would be blocked.
61
106
  const beacon = generateAstroidVitalsBeacon(config, cwvBeaconScript());
62
107
  files.push({ path: beacon.path, contents: beacon.contents });
@@ -68,15 +113,26 @@ export function generateAstroidScaffoldFiles(config) {
68
113
  files.push({
69
114
  path: "src/schema.site.ts",
70
115
  contents: [
71
- "// Site-owned Drizzle tables — the ones Astroid doesn't manage. Declare them",
116
+ "// Site-owned Drizzle tables—the ones Astroid doesn't manage. Declare them",
72
117
  "// here; the generated src/schema.ts re-exports everything from this file, so",
73
118
  "// drizzle-kit sees them and the worker can import them. Empty by default.",
74
119
  "//",
75
- '// e.g. export const redirects = sqliteTable("redirects", { … });',
120
+ '// For example: export const redirects = sqliteTable("redirects", { … });',
76
121
  "export {};",
77
122
  "",
78
123
  ].join("\n"),
79
124
  });
125
+ // --- pages: the transform + reserved-slugs seam ---------------------------
126
+ if (config.pages?.hooks) {
127
+ files.push({ path: "src/pages-hooks.ts", contents: generateAstroidPagesHooks() });
128
+ }
129
+ // --- settings: the sanitize + read seam ------------------------------------
130
+ // Only when asked for (settings.hooks): the generated worker and the Actions
131
+ // surface both import it, so it must exist whenever either does. Scaffolded
132
+ // empty, which leaves the route exactly as it is without hooks.
133
+ if (config.settings?.hooks) {
134
+ files.push({ path: "src/settings-hooks.ts", contents: generateAstroidSettingsHooks() });
135
+ }
80
136
  // --- the typed Astro Actions surface --------------------------------------
81
137
  // Always: every project has editable pages, and the routes alone leave the
82
138
  // Astro-native half of ADR 0001 unbuilt. Scaffold-once because it is meant to
@@ -96,7 +152,7 @@ export function generateAstroidScaffoldFiles(config) {
96
152
  // is why `astroid generate` must never rewrite them.
97
153
  if (astroidUsesQueues(config)) {
98
154
  files.push({ path: "src/queue.ts", contents: generateAstroidQueueSeam(config) });
99
- // One receiver per provider — a site can run two (invoicing + storefront).
155
+ // One receiver per provider—a site can run two (invoicing + storefront).
100
156
  for (const route of generateAstroidWebhookRoutes(config)) {
101
157
  files.push({ path: route.path, contents: route.contents });
102
158
  }
@@ -107,14 +163,14 @@ export function generateAstroidScaffoldFiles(config) {
107
163
  if (gallery)
108
164
  files.push({ path: "src/pages/work.astro", contents: gallery });
109
165
  // --- pwa: the service worker, manifest, and its headers -------------------
110
- // Static files under public/, not generated source — a service worker is not
166
+ // Static files under public/, not generated source—a service worker is not
111
167
  // bundled, and `_headers` is shared with whatever else writes to it.
112
168
  const sw = generateServiceWorker(config);
113
169
  if (sw) {
114
170
  // `emitDir` puts them where the BROWSER will ask for them. A PWA on its own
115
171
  // subdomain that rewrites to a path prefix (studio.example.com/ → /studio/)
116
- // fetches /sw.js at its own origin root, which rewrites to /studio/sw.js —
117
- // so a worker emitted at the public root is a 404 nothing explains.
172
+ // fetches /sw.js at its own origin root, which rewrites to /studio/sw.js—so
173
+ // a worker emitted at the public root is a 404 nothing explains.
118
174
  const pwaDir = resolvePwa(config).emitDir;
119
175
  const publicBase = pwaDir ? `public/${pwaDir}` : "public";
120
176
  files.push({ path: `${publicBase}/sw.js`, contents: sw });
@@ -125,7 +181,7 @@ export function generateAstroidScaffoldFiles(config) {
125
181
  const headers = generatePwaHeaders(config);
126
182
  if (headers) {
127
183
  files.push({
128
- // `_headers` stays at the public root wherever the worker lives — it is
184
+ // `_headers` stays at the public root wherever the worker lives—it is
129
185
  // one file for the whole site, and Cloudflare only reads it there.
130
186
  path: "public/_headers",
131
187
  contents: headers,
@@ -154,7 +210,7 @@ export function generateAstroidScaffoldFiles(config) {
154
210
  files.push({ path: "src/edit-session.ts", contents: editSession });
155
211
  // --- tenancy: what a subdomain maps to ------------------------------------
156
212
  // Astroid owns the wildcard route and the middleware wiring; this file owns
157
- // every decision — the lookup, its caching, and what an unknown host means.
213
+ // every decision—the lookup, its caching, and what an unknown host means.
158
214
  const tenancy = generateAstroidTenancy(config);
159
215
  if (tenancy)
160
216
  files.push({ path: "src/tenancy.ts", contents: tenancy });
@@ -167,7 +223,7 @@ export function generateAstroidScaffoldFiles(config) {
167
223
  const route = generateAstroidPortalAuthRoute(config);
168
224
  // Always non-null alongside portalAuth (same `astroidPortal` gate), but the
169
225
  // types don't know that and a silent drop here is a portal that cannot
170
- // authenticate — so assert it rather than `?.`-ing it away.
226
+ // authenticate—so assert it rather than `?.`-ing it away.
171
227
  //
172
228
  // The route file lives at the mount path, because Astro routing is
173
229
  // file-path-based: a portal mounted at `/api/shop-auth` needs its catch-all
@@ -0,0 +1,22 @@
1
+ import type { AstroidConfig, SectionKind } from "../config.js";
2
+ /** A stored section item: its `_type`, field values, and settings tokens. */
3
+ export interface AstroidSeedSection {
4
+ _type: SectionKind;
5
+ _settings?: Record<string, string>;
6
+ [field: string]: unknown;
7
+ }
8
+ /**
9
+ * The sections a new project's home page starts with: the config's `sections`,
10
+ * or its archetype's default when the config doesn't list any, each with
11
+ * sample content.
12
+ */
13
+ export declare function astroidHomeSeedSections(config: AstroidConfig): AstroidSeedSection[];
14
+ /**
15
+ * `seed/home.seed.sql`: one idempotent insert of the published `home` page,
16
+ * with the sections from {@link astroidHomeSeedSections}.
17
+ *
18
+ * Every value is escaped here, so a brand name with an apostrophe produces
19
+ * valid SQL. The template's token substitution can't escape, which is why the
20
+ * seed isn't a template file.
21
+ */
22
+ export declare function generateAstroidHomeSeed(config: AstroidConfig): string;
@@ -0,0 +1,214 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ //
3
+ // The home page a new project seeds, built from its own config.
4
+ //
5
+ // This used to be a fixed file in the `create-astroid` template, and it seeded
6
+ // the marketing sections (`hero`, `featureGrid`, `cta`) for every archetype. A
7
+ // portfolio's config listed `hero`, `gallery`, `aboutIntro`, and `contact`, but
8
+ // its first page showed a feature grid and a call to action instead. Building
9
+ // the seed from the same section list the config holds means the two can't
10
+ // disagree.
11
+ //
12
+ // It isn't in `generateAstroidScaffoldFiles`, on purpose: a seed runs once,
13
+ // against a fresh database, so `astroid generate` has no reason to write it
14
+ // back into a project that deleted it.
15
+ import { ASTROID_ARCHETYPE_SECTIONS } from "../config.js";
16
+ /**
17
+ * Sample content for every section kind, keyed by `SectionKind`.
18
+ *
19
+ * A `Record` over the whole vocabulary rather than just the kinds an archetype
20
+ * uses today, so adding a section to the catalog is a compile error here until
21
+ * it has a sample. That keeps a site's `sections` override seedable, whatever
22
+ * it lists.
23
+ *
24
+ * The copy tells the owner how to replace it, because it's the first thing they
25
+ * see after signing in. Images are left empty: a new project has no media, and
26
+ * every section with an image renders without one.
27
+ */
28
+ const SAMPLES = {
29
+ hero: (brand) => ({
30
+ heading: brand,
31
+ subheading: "Sign in, switch on edit mode, and type to change this text.",
32
+ ctaLabel: "Get in touch",
33
+ ctaHref: "/contact",
34
+ _settings: { colorway: "base", align: "center" },
35
+ }),
36
+ featureGrid: () => ({
37
+ heading: "What you offer",
38
+ items: [
39
+ { title: "First thing", body: "Describe it here." },
40
+ { title: "Second thing", body: "And this one." },
41
+ { title: "Third thing", body: "And this one too." },
42
+ ],
43
+ _settings: { colorway: "base" },
44
+ }),
45
+ cta: () => ({
46
+ heading: "Ready when you are",
47
+ body: "Replace this copy with your own.",
48
+ ctaLabel: "Contact us",
49
+ ctaHref: "/contact",
50
+ _settings: { colorway: "brand", align: "center" },
51
+ }),
52
+ gallery: () => ({
53
+ heading: "Selected work",
54
+ items: [],
55
+ _settings: { colorway: "base" },
56
+ }),
57
+ media: () => ({
58
+ heading: "A closer look",
59
+ _settings: { colorway: "base" },
60
+ }),
61
+ splitImage: () => ({
62
+ heading: "Tell your story",
63
+ body: "<p>Add an image and a few sentences about what makes this place yours.</p>",
64
+ _layout: "imageStart",
65
+ _settings: { colorway: "base" },
66
+ }),
67
+ steps: () => ({
68
+ heading: "How it works",
69
+ items: [
70
+ { title: "Get in touch", body: "Say what you need." },
71
+ { title: "Plan it together", body: "Agree on the details." },
72
+ { title: "Done", body: "Describe the result here." },
73
+ ],
74
+ _settings: { colorway: "base" },
75
+ }),
76
+ banner: () => ({
77
+ text: "Announce something here, like new hours or a seasonal special.",
78
+ _settings: { colorway: "brand", align: "center" },
79
+ }),
80
+ faq: () => ({
81
+ heading: "Questions",
82
+ items: [
83
+ { question: "What's the first question people ask?", answer: "<p>Answer it here.</p>" },
84
+ { question: "And the second?", answer: "<p>Answer that one here.</p>" },
85
+ ],
86
+ _settings: { colorway: "base" },
87
+ }),
88
+ pricingTiers: () => ({
89
+ heading: "Pricing",
90
+ items: [
91
+ {
92
+ name: "Starter",
93
+ price: "$10",
94
+ period: "/mo",
95
+ features: [{ text: "Describe what's included" }],
96
+ featured: "no",
97
+ },
98
+ {
99
+ name: "Plus",
100
+ price: "$20",
101
+ period: "/mo",
102
+ features: [{ text: "Everything in Starter" }, { text: "And more" }],
103
+ featured: "yes",
104
+ },
105
+ ],
106
+ _settings: { colorway: "base" },
107
+ }),
108
+ testimonial: () => ({
109
+ quote: "Put a favorite thing a customer said about you here.",
110
+ attribution: "Alex",
111
+ role: "Customer",
112
+ _settings: { colorway: "base", align: "center" },
113
+ }),
114
+ aboutIntro: (brand) => ({
115
+ heading: `About ${brand}`,
116
+ body: "<p>Say who you are, what you make, and why it matters to you.</p>",
117
+ _settings: { colorway: "base" },
118
+ }),
119
+ productGrid: () => ({
120
+ heading: "Featured",
121
+ items: [
122
+ { name: "First product", price: "$12" },
123
+ { name: "Second product", price: "$18" },
124
+ { name: "Third product", price: "$24" },
125
+ ],
126
+ _settings: { colorway: "base" },
127
+ }),
128
+ locationHours: () => ({
129
+ heading: "Visit",
130
+ address: "Your street address\nCity, region, and postal code",
131
+ phone: "800-555-0100",
132
+ items: [
133
+ { day: "Monday to Friday", hours: "8 AM to 6 PM" },
134
+ { day: "Saturday and Sunday", hours: "9 AM to 4 PM" },
135
+ ],
136
+ _settings: { colorway: "base" },
137
+ }),
138
+ contact: () => ({
139
+ heading: "Get in touch",
140
+ blurb: "Questions, orders, or just saying hello: send a message.",
141
+ _settings: { colorway: "secondary" },
142
+ }),
143
+ };
144
+ /**
145
+ * The sections a new project's home page starts with: the config's `sections`,
146
+ * or its archetype's default when the config doesn't list any, each with
147
+ * sample content.
148
+ */
149
+ export function astroidHomeSeedSections(config) {
150
+ const kinds = config.sections ?? ASTROID_ARCHETYPE_SECTIONS[config.archetype];
151
+ return kinds.map((kind) => ({ _type: kind, ...SAMPLES[kind](config.theme.name) }));
152
+ }
153
+ /** A SQLite string literal. Doubling `'` is the only escape SQLite needs. */
154
+ function sqlString(value) {
155
+ return `'${value.replaceAll("'", "''")}'`;
156
+ }
157
+ /**
158
+ * `seed/home.seed.sql`: one idempotent insert of the published `home` page,
159
+ * with the sections from {@link astroidHomeSeedSections}.
160
+ *
161
+ * Every value is escaped here, so a brand name with an apostrophe produces
162
+ * valid SQL. The template's token substitution can't escape, which is why the
163
+ * seed isn't a template file.
164
+ */
165
+ export function generateAstroidHomeSeed(config) {
166
+ const brand = config.theme.name;
167
+ const sections = JSON.stringify(astroidHomeSeedSections(config), null, 2);
168
+ const body = `<p>Welcome to ${escapeHtml(brand)}. Sign in at <code>/login</code>, switch on edit mode, and change this text in place, then select Publish.</p>`;
169
+ return [
170
+ "-- Seed the editable home page (slug `home`) that src/pages/index.astro renders.",
171
+ "-- Idempotent. Apply once after migrations:",
172
+ "-- wrangler d1 execute DB --file seed/home.seed.sql --remote",
173
+ "-- (drop --remote for the local dev D1)",
174
+ "--",
175
+ "-- `sections` is the page-builder array: the same shape the on-canvas editor",
176
+ "-- reads and the server validates against the section catalog on write. Each item",
177
+ '-- is `{"_type": …, …fields, "_settings": {…}}`, and `_settings` holds TOKENS',
178
+ "-- (colorway, align), never CSS, so a re-theme needs no content change. The list",
179
+ "-- matches `sections` in astroid.config.ts.",
180
+ "--",
181
+ "-- Seeding it is deliberate: an empty array renders an empty page, and a blank",
182
+ "-- canvas is a worse first run than having something real to click on and edit.",
183
+ "--",
184
+ "-- Full-text search (the editor's Pages search) is kept in sync on PUBLISH, so a",
185
+ "-- row inserted with raw SQL like this isn't in the index until you either",
186
+ "-- publish an edit to it or backfill once with `POST /api/louise/pages/reindex`",
187
+ "-- (signed in). The page renders fine either way; only in-editor search is",
188
+ "-- affected.",
189
+ "--",
190
+ "-- To override just the <head> title and description (not the on-page H1), add",
191
+ "-- `seo_title` and `seo_description` columns below. `seo_title` is run through the",
192
+ "-- config's title template, which already appends the brand, so set the page",
193
+ '-- part only ("Pricing"), not "Pricing | Your brand".',
194
+ "INSERT OR IGNORE INTO pages (slug, title, body, sections, status, sort_order, created_at, updated_at)",
195
+ "VALUES (",
196
+ " 'home',",
197
+ ` ${sqlString(brand)},`,
198
+ ` ${sqlString(body)},`,
199
+ ` json(${sqlString(sections)}),`,
200
+ " 'published',",
201
+ " 0,",
202
+ " unixepoch(),",
203
+ " unixepoch()",
204
+ ");",
205
+ "",
206
+ ].join("\n");
207
+ }
208
+ function escapeHtml(value) {
209
+ return value
210
+ .replaceAll("&", "&amp;")
211
+ .replaceAll("<", "&lt;")
212
+ .replaceAll(">", "&gt;")
213
+ .replaceAll('"', "&quot;");
214
+ }
@@ -21,38 +21,38 @@ export interface PwaConfig {
21
21
  /** Extra paths to precache alongside the scope root. */
22
22
  shell?: string[];
23
23
  /**
24
- * A prerendered page to serve when a navigation fails offline, e.g.
24
+ * A prerendered page to serve when a navigation fails offline, for example,
25
25
  * `"/offline"`.
26
26
  *
27
- * Without it the fallback is the scope root — the *dynamic* app shell, which
27
+ * Without it the fallback is the scope root—the *dynamic* app shell, which
28
28
  * is exactly the wrong thing to precache when the app is auth-gated: that
29
29
  * response carries `Cache-Control: no-store`, so either nothing is cached and
30
30
  * the fallback is empty, or a signed-in shell is stored and later served to
31
31
  * whoever opens the app next.
32
32
  *
33
33
  * Point it at a page with no session-specific markup. It is precached with the
34
- * shell, so it must be prerendered — a dynamic route here fails at exactly the
34
+ * shell, so it must be prerendered—a dynamic route here fails at exactly the
35
35
  * moment it is needed.
36
36
  */
37
37
  offlineFallback?: string;
38
38
  /**
39
- * Subdirectory under `public/` to emit `sw.js` and the manifest into, e.g.
39
+ * Subdirectory under `public/` to emit `sw.js` and the manifest into, for example,
40
40
  * `"studio"`. Default: the public root.
41
41
  *
42
- * For a PWA served from its own subdomain that rewrites to a path prefix —
43
- * `studio.example.com/` → `/studio/` — the browser fetches `/sw.js` at *its*
42
+ * For a PWA served from its own subdomain that rewrites to a path prefix—`studio.example.com/`
43
+ * → `/studio/`—the browser fetches `/sw.js` at *its*
44
44
  * origin root, which rewrites to `/studio/sw.js`. Emitting at the public root
45
45
  * puts the file where nothing will ask for it.
46
46
  *
47
47
  * Set this to the same prefix the host rewrites to. With `scope` equal to the
48
- * serving path, no `Service-Worker-Allowed` header is needed — a worker may
48
+ * serving path, no `Service-Worker-Allowed` header is needed—a worker may
49
49
  * always control its own directory and below.
50
50
  */
51
51
  emitDir?: string;
52
52
  }
53
53
  /** True when this project switched the PWA on. */
54
54
  export declare const usesPwa: (config: AstroidConfig) => boolean;
55
- /** Resolved PWA settings — config over derivation over default. */
55
+ /** Resolved PWA settings—config over derivation over default. */
56
56
  export declare function resolvePwa(config: AstroidConfig): Required<Omit<PwaConfig, "shell" | "offlineFallback" | "emitDir">> & {
57
57
  shell: string[];
58
58
  /** `null` when the app falls back to the scope root (see the config note). */
@@ -63,18 +63,18 @@ export declare function resolvePwa(config: AstroidConfig): Required<Omit<PwaConf
63
63
  /**
64
64
  * `public/manifest.webmanifest`.
65
65
  *
66
- * Icons are declared but NOT generated — a brand's icon is not something a
66
+ * Icons are declared but NOT generated—a brand's icon is not something a
67
67
  * scaffold can invent, and emitting placeholders would produce an installable
68
68
  * app with a grey square for a face. The generated README step says to add them.
69
69
  */
70
70
  export declare function generateWebManifest(config: AstroidConfig): string | null;
71
- /** `public/sw.js`. Plain JS — a service worker is not bundled. */
71
+ /** `public/sw.js`. Plain JS—a service worker is not bundled. */
72
72
  export declare function generateServiceWorker(config: AstroidConfig): string | null;
73
73
  /**
74
74
  * The `public/_headers` block the PWA needs.
75
75
  *
76
76
  * `Service-Worker-Allowed` is emitted ONLY when the scope is broader than the
77
- * script's own location — which, with `sw.js` at the root, never is. Emitting it
77
+ * script's own location—which, with `sw.js` at the root, never is. Emitting it
78
78
  * unconditionally (as the reference does) is harmless but misleading: it implies
79
79
  * a requirement that isn't there, and someone later moving the script will trust
80
80
  * a header that no longer says what they need.
@@ -1,21 +1,21 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // The PWA scaffold — a scoped service worker, a manifest, and the headers they
3
+ // The PWA scaffold—a scoped service worker, a manifest, and the headers they
4
4
  // need.
5
5
  //
6
6
  // The scoping is the whole design, not a detail. A Louise site is CMS-edited:
7
7
  // an editor signs in, flips edit mode on, and edits the live page in place. A
8
8
  // service worker that cached HTML across the whole origin would serve that
9
- // editor a stale copy of the page they are trying to change — and the bug would
9
+ // editor a stale copy of the page they are trying to change—and the bug would
10
10
  // present as "my edits don't save", which is about as far from the cause as a
11
11
  // report can get.
12
12
  //
13
13
  // So the generated worker is scoped, and inside its scope it still refuses to
14
14
  // touch anything dynamic:
15
15
  //
16
- // • `/api/*` never cached — checkout, auth, and every Louise write
17
- // • editor routes never cached — the studio must always be live
18
- // • edit-mode URLs never cached — `?louise` marks a request as an editing
16
+ // • `/api/*` never cached—checkout, auth, and every Louise write
17
+ // • editor routes never cached—the studio must always be live
18
+ // • edit-mode URLs never cached—`?louise` marks a request as an editing
19
19
  // session, and caching one poisons it for everyone
20
20
  //
21
21
  // Everything else is the ordinary split: navigations network-first with a
@@ -23,7 +23,7 @@
23
23
  // when their content does.
24
24
  /** True when this project switched the PWA on. */
25
25
  export const usesPwa = (config) => (config.modules ?? []).includes("pwa");
26
- /** Resolved PWA settings — config over derivation over default. */
26
+ /** Resolved PWA settings—config over derivation over default. */
27
27
  export function resolvePwa(config) {
28
28
  const pwa = config.pwa ?? {};
29
29
  // Normalized to a leading slash and no trailing one (except root), because
@@ -41,7 +41,7 @@ export function resolvePwa(config) {
41
41
  themeColor: pwa.themeColor ?? config.theme.colors.brand,
42
42
  offlineFallback: pwa.offlineFallback ?? null,
43
43
  emitDir: (pwa.emitDir ?? "").replace(/^\/+|\/+$/g, ""),
44
- // The offline page is precached with the shell — a fallback fetched on
44
+ // The offline page is precached with the shell—a fallback fetched on
45
45
  // demand is a fallback that isn't there when the network is.
46
46
  shell: [
47
47
  scope,
@@ -51,7 +51,7 @@ export function resolvePwa(config) {
51
51
  ],
52
52
  };
53
53
  }
54
- /** URL prefix the emitted `sw.js` + manifest are served from — `""` at the
54
+ /** URL prefix the emitted `sw.js` + manifest are served from—`""` at the
55
55
  * public root, `"/studio"` under an `emitDir`. */
56
56
  function assetBase(emitDir) {
57
57
  const dir = (emitDir ?? "").replace(/^\/+|\/+$/g, "");
@@ -60,7 +60,7 @@ function assetBase(emitDir) {
60
60
  /**
61
61
  * `public/manifest.webmanifest`.
62
62
  *
63
- * Icons are declared but NOT generated — a brand's icon is not something a
63
+ * Icons are declared but NOT generated—a brand's icon is not something a
64
64
  * scaffold can invent, and emitting placeholders would produce an installable
65
65
  * app with a grey square for a face. The generated README step says to add them.
66
66
  */
@@ -100,7 +100,7 @@ export function generateWebManifest(config) {
100
100
  ],
101
101
  }, null, 2)}\n`;
102
102
  }
103
- /** `public/sw.js`. Plain JS — a service worker is not bundled. */
103
+ /** `public/sw.js`. Plain JS—a service worker is not bundled. */
104
104
  export function generateServiceWorker(config) {
105
105
  if (!usesPwa(config))
106
106
  return null;
@@ -111,11 +111,11 @@ export function generateServiceWorker(config) {
111
111
  return [
112
112
  `// Service worker for the ${config.theme.name} PWA, scoped to ${pwa.scope}.`,
113
113
  "//",
114
- "// Generated by Astroid. Safe to edit — bump CACHE to invalidate everything",
114
+ "// Generated by Astroid. Safe to edit—bump CACHE to invalidate everything",
115
115
  "// on the next visit.",
116
116
  "//",
117
117
  "// What it deliberately never caches, and why:",
118
- "// /api/* checkout, auth, and every Louise write — a cached POST",
118
+ "// /api/* checkout, auth, and every Louise write—a cached POST",
119
119
  "// response or a stale session is worse than being offline",
120
120
  "// editor routes the studio must always be live",
121
121
  "// ?louise URLs an edit-mode request; caching one would serve an editor a",
@@ -126,8 +126,8 @@ export function generateServiceWorker(config) {
126
126
  ...(pwa.offlineFallback
127
127
  ? [
128
128
  "// A prerendered page with no session-specific markup. The scope root is",
129
- "// the app SHELL, which on an auth-gated app is `Cache-Control: no-store`",
130
- "// — so falling back to it serves either nothing or someone else's shell.",
129
+ "// the app SHELL, which on an auth-gated app is `Cache-Control: no-store`—",
130
+ "// so falling back to it serves either nothing or someone else's shell.",
131
131
  `const OFFLINE = ${JSON.stringify(pwa.offlineFallback)};`,
132
132
  ]
133
133
  : []),
@@ -165,7 +165,7 @@ export function generateServiceWorker(config) {
165
165
  " );",
166
166
  "}",
167
167
  "",
168
- "/** Hashed build output — the filename changes when the content does, so it",
168
+ "/** Hashed build output—the filename changes when the content does, so it",
169
169
  " * can be cached forever without a staleness risk. */",
170
170
  "function isImmutable(url) {",
171
171
  " return url.pathname.startsWith('/_astro/') || url.pathname.startsWith('/icons/');",
@@ -181,7 +181,7 @@ export function generateServiceWorker(config) {
181
181
  " // Cross-origin requests belong to whoever serves them.",
182
182
  " if (url.origin !== self.location.origin) return;",
183
183
  " if (isDynamic(url)) return;",
184
- " // Outside the scope this worker has no business intercepting — the rest of",
184
+ " // Outside the scope this worker has no business intercepting—the rest of",
185
185
  " // the site is CMS-edited and must stay live.",
186
186
  " if (SCOPE !== '/' && !url.pathname.startsWith(SCOPE)) return;",
187
187
  "",
@@ -230,7 +230,7 @@ export function generateServiceWorker(config) {
230
230
  * The `public/_headers` block the PWA needs.
231
231
  *
232
232
  * `Service-Worker-Allowed` is emitted ONLY when the scope is broader than the
233
- * script's own location — which, with `sw.js` at the root, never is. Emitting it
233
+ * script's own location—which, with `sw.js` at the root, never is. Emitting it
234
234
  * unconditionally (as the reference does) is harmless but misleading: it implies
235
235
  * a requirement that isn't there, and someone later moving the script will trust
236
236
  * a header that no longer says what they need.
@@ -238,14 +238,14 @@ export function generateServiceWorker(config) {
238
238
  export function generatePwaHeaders(config) {
239
239
  if (!usesPwa(config))
240
240
  return null;
241
- // Paths must match where the files are actually emitted — a stanza for
241
+ // Paths must match where the files are actually emitted—a stanza for
242
242
  // `/sw.js` while the worker lives at `/studio/sw.js` sets headers on nothing,
243
243
  // and the no-cache rule is what stops a bad worker sticking around.
244
244
  const base = assetBase(config.pwa?.emitDir);
245
245
  return [
246
246
  "",
247
247
  "# The service worker must revalidate on every load, or a bad worker sticks",
248
- "# around until its cache entry expires — and it controls every page in scope.",
248
+ "# around until its cache entry expires—and it controls every page in scope.",
249
249
  `${base}/sw.js`,
250
250
  " Cache-Control: no-cache",
251
251
  "",
@@ -1,12 +1,12 @@
1
1
  import { type AstroidQueueMessage } from "./messages.js";
2
2
  export interface QueueHandlerOptions {
3
3
  /**
4
- * Re-sync whatever the provider owns — the catalog mirror, a cache. Called
4
+ * Re-sync whatever the provider owns—the catalog mirror, a cache. Called
5
5
  * for a periodic refresh and for webhooks that touched the catalog.
6
6
  *
7
7
  * Receives the message that triggered it, so a site running more than one
8
8
  * commerce provider can branch on `message.provider` rather than refreshing
9
- * everything for everything. A zero-argument seam stays valid — the parameter
9
+ * everything for everything. A zero-argument seam stays valid—the parameter
10
10
  * is there to be ignored until it's needed.
11
11
  *
12
12
  * Throwing marks the message for retry, which is usually right: a failed
@@ -24,7 +24,7 @@ export interface QueueHandlerOptions {
24
24
  * storefront and Square as its POS, at which point `refreshCatalog` means
25
25
  * "re-pull Fourthwall" while Square emits `inventory.count.updated` on every
26
26
  * single sale. Unscoped, a good Saturday becomes a sync storm against an
27
- * unrelated provider's rate limit — and the periodic refresh is unaffected, so
27
+ * unrelated provider's rate limit—and the periodic refresh is unaffected, so
28
28
  * the site looks fine until the day it's busy.
29
29
  */
30
30
  catalogProvider?: string;
@@ -9,8 +9,8 @@
9
9
  // acks as a no-op.
10
10
  //
11
11
  // That last part matters more than it looks. Order, payment, and subscription
12
- // events are read live from the provider, so there is nothing local to update —
13
- // but they still arrive, in volume. A consumer that treats every event as
12
+ // events are read live from the provider, so there is nothing local to
13
+ // update—but they still arrive, in volume. A consumer that treats every event as
14
14
  // actionable turns a busy sales day into a catalog-refresh storm.
15
15
  import { affectsCatalog } from "./messages.js";
16
16
  /**