astroidjs 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -42
- package/bin/astroid.mjs +25 -25
- package/dist/analytics/index.d.ts +3 -3
- package/dist/analytics/index.js +9 -9
- package/dist/astro/csp.d.ts +5 -5
- package/dist/astro/csp.js +6 -6
- package/dist/astro/index.js +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +5 -5
- package/dist/commerce/adapters.d.ts +6 -6
- package/dist/commerce/adapters.js +9 -9
- package/dist/commerce/checkout-scaffold.d.ts +5 -5
- package/dist/commerce/checkout-scaffold.js +9 -9
- package/dist/commerce/checkout.d.ts +12 -12
- package/dist/commerce/checkout.js +7 -7
- package/dist/commerce/loader.d.ts +2 -2
- package/dist/commerce/loader.js +3 -3
- package/dist/commerce/mirror.d.ts +4 -4
- package/dist/commerce/mirror.js +9 -9
- package/dist/commerce/roles.d.ts +10 -10
- package/dist/commerce/roles.js +13 -13
- package/dist/commerce/secrets.d.ts +9 -9
- package/dist/commerce/secrets.js +9 -9
- package/dist/commerce/sync.d.ts +7 -7
- package/dist/commerce/sync.js +5 -5
- package/dist/components/sections.d.ts +9 -9
- package/dist/components/sections.js +12 -12
- package/dist/config.d.ts +62 -62
- package/dist/config.js +18 -18
- package/dist/email/inquiry.d.ts +2 -2
- package/dist/email/inquiry.js +1 -1
- package/dist/email/send.d.ts +4 -4
- package/dist/email/send.js +7 -7
- package/dist/email/templates.js +3 -3
- package/dist/email/theme.d.ts +1 -1
- package/dist/email/theme.js +4 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.js +1 -1
- package/dist/map/pmtiles.d.ts +5 -5
- package/dist/map/pmtiles.js +5 -5
- package/dist/map/scaffold.d.ts +2 -2
- package/dist/map/scaffold.js +4 -4
- package/dist/map/style.d.ts +4 -4
- package/dist/map/style.js +1 -1
- package/dist/portal/config.d.ts +2 -2
- package/dist/portal/config.js +3 -3
- package/dist/portal/guard.d.ts +4 -4
- package/dist/portal/guard.js +4 -4
- package/dist/portal/nav.js +2 -2
- package/dist/portal/scaffold.d.ts +4 -4
- package/dist/portal/scaffold.js +6 -6
- package/dist/portal/session.d.ts +2 -2
- package/dist/portal/session.js +5 -5
- package/dist/portfolio/scaffold.d.ts +1 -1
- package/dist/portfolio/scaffold.js +4 -4
- package/dist/project/actions.d.ts +1 -1
- package/dist/project/actions.js +6 -6
- package/dist/project/generate.d.ts +4 -4
- package/dist/project/generate.js +15 -15
- package/dist/project/index.js +1 -1
- package/dist/project/scaffold.d.ts +2 -2
- package/dist/project/scaffold.js +11 -11
- package/dist/pwa/generate.d.ts +11 -11
- package/dist/pwa/generate.js +12 -12
- package/dist/queues/consumer.d.ts +3 -3
- package/dist/queues/consumer.js +2 -2
- package/dist/queues/messages.d.ts +4 -4
- package/dist/queues/messages.js +2 -2
- package/dist/queues/scaffold.d.ts +4 -4
- package/dist/queues/scaffold.js +8 -8
- package/dist/queues/webhook.d.ts +5 -5
- package/dist/queues/webhook.js +3 -3
- package/dist/realtime/scaffold.d.ts +4 -4
- package/dist/realtime/scaffold.js +8 -8
- package/dist/schema/collections.d.ts +6 -6
- package/dist/schema/collections.js +23 -23
- package/dist/schema/framework.d.ts +1 -1
- package/dist/schema/framework.js +2 -2
- package/dist/schema/generate.js +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/secrets.d.ts +6 -6
- package/dist/secrets.js +6 -6
- package/dist/security/csp-origins.d.ts +1 -1
- package/dist/security/csp-origins.js +3 -3
- package/dist/security/rate-rules.d.ts +2 -2
- package/dist/security/rate-rules.js +8 -8
- package/dist/seo/resolve.d.ts +5 -5
- package/dist/seo/resolve.js +2 -2
- package/dist/seo/routes.d.ts +5 -5
- package/dist/seo/routes.js +3 -3
- package/dist/seo/structured-data.d.ts +6 -6
- package/dist/seo/structured-data.js +7 -7
- package/dist/status.d.ts +5 -5
- package/dist/status.js +7 -7
- package/dist/tenancy/index.d.ts +3 -3
- package/dist/tenancy/index.js +6 -6
- package/dist/worker/generate.d.ts +2 -2
- package/dist/worker/generate.js +19 -19
- package/dist/worker/index.js +1 -1
- package/dist/worker/routes.js +1 -1
- package/dist/workflow/advance.d.ts +3 -3
- package/dist/workflow/advance.js +6 -6
- package/dist/workflow/config.d.ts +4 -4
- package/dist/workflow/config.js +4 -4
- package/dist/workflow/generate.d.ts +2 -2
- package/dist/workflow/generate.js +4 -4
- package/package.json +5 -5
- package/src/components/Collection.tsx +5 -5
- package/src/components/Editable.astro +9 -9
- package/src/components/JustifiedGallery.astro +8 -8
- package/src/components/MediaSlot.astro +12 -12
- package/src/components/PortalShell.astro +4 -4
- package/src/components/RegisterSW.astro +3 -3
- package/src/components/Section.astro +8 -8
- package/src/components/Sections.astro +6 -6
- package/src/components/Seo.astro +3 -3
- package/src/components/StageBar.astro +3 -3
- package/src/components/StructuredData.astro +2 -2
- package/src/components/justify.ts +9 -9
- package/src/components/media-meta.ts +10 -10
- package/src/components/sections/AboutIntro.astro +1 -1
- package/src/components/sections/Contact.astro +1 -1
- package/src/components/sections/Cta.astro +1 -1
- package/src/components/sections/Faq.astro +1 -1
- package/src/components/sections/FeatureGrid.astro +2 -2
- package/src/components/sections/Hero.astro +1 -1
- package/src/components/sections/PricingTiers.astro +1 -1
- package/src/components/sections/ProductGrid.astro +1 -1
- package/src/components/sections/SplitImage.astro +1 -1
- package/src/components/sections/Steps.astro +1 -1
- package/src/components/sections/Testimonial.astro +1 -1
- package/src/components/sections.ts +17 -17
package/dist/queues/webhook.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AstroidQueueMessage } from "./messages.js";
|
|
2
2
|
/**
|
|
3
|
-
* The queue producer surface used here
|
|
3
|
+
* The queue producer surface used here—structural, so a real `Queue<T>`
|
|
4
4
|
* binding satisfies it without astroid depending on the Workers types.
|
|
5
5
|
*
|
|
6
6
|
* `Promise<unknown>` rather than `Promise<void>`: Cloudflare's `Queue.send`
|
|
@@ -15,18 +15,18 @@ export interface WebhookVerifyInput {
|
|
|
15
15
|
raw: string;
|
|
16
16
|
headers: Headers;
|
|
17
17
|
url: URL;
|
|
18
|
-
/** The signing secret
|
|
18
|
+
/** The signing secret—already checked to be real by the caller. */
|
|
19
19
|
secret: string;
|
|
20
20
|
}
|
|
21
21
|
export interface WebhookRouteOptions {
|
|
22
|
-
/** Which integration this endpoint serves
|
|
22
|
+
/** Which integration this endpoint serves—carried into the message. */
|
|
23
23
|
provider: string;
|
|
24
24
|
/**
|
|
25
25
|
* The signing secret, or `null` when unprovisioned. Read it with
|
|
26
26
|
* `readModuleSecret` so a placeholder counts as absent.
|
|
27
27
|
*/
|
|
28
28
|
secret: string | null;
|
|
29
|
-
/** Signature check over the raw body
|
|
29
|
+
/** Signature check over the raw body—for example, `verifySquareSignature`. */
|
|
30
30
|
verify: (input: WebhookVerifyInput) => boolean | Promise<boolean>;
|
|
31
31
|
/** The queue binding, or null/undefined when Queues aren't provisioned. */
|
|
32
32
|
queue?: QueueProducer | null;
|
|
@@ -38,7 +38,7 @@ export interface WebhookRouteOptions {
|
|
|
38
38
|
eventType?: (payload: unknown) => string;
|
|
39
39
|
/**
|
|
40
40
|
* Decide whether an event is worth queueing at all. Returning false acks the
|
|
41
|
-
* delivery without enqueuing
|
|
41
|
+
* delivery without enqueuing—the provider is satisfied and the consumer
|
|
42
42
|
* isn't woken for an event nothing acts on.
|
|
43
43
|
*/
|
|
44
44
|
accept?: (type: string, payload: unknown) => boolean;
|
package/dist/queues/webhook.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// All three sites wrote this route the same way, and the ordering is the part
|
|
6
6
|
// worth encoding. **Verify the HMAC over the raw body before parsing anything.**
|
|
7
|
-
// Not for style
|
|
7
|
+
// Not for style—parsing first means an unauthenticated caller can reach the
|
|
8
8
|
// JSON parser and everything downstream of it, and re-serializing a parsed body
|
|
9
9
|
// to check the signature is how signature checks quietly stop checking anything.
|
|
10
10
|
// So the raw text is read once, verified, and only then parsed.
|
|
@@ -58,7 +58,7 @@ export async function handleWebhook(request, url, options) {
|
|
|
58
58
|
payload = JSON.parse(raw);
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
61
|
-
// Also terminal
|
|
61
|
+
// Also terminal—a body that isn't JSON now won't become JSON later.
|
|
62
62
|
return text("Invalid JSON", 400);
|
|
63
63
|
}
|
|
64
64
|
const type = (options.eventType ?? defaultEventType)(payload);
|
|
@@ -76,6 +76,6 @@ export async function handleWebhook(request, url, options) {
|
|
|
76
76
|
return text("Queue unavailable", 503);
|
|
77
77
|
}
|
|
78
78
|
// 202, not 200: the work hasn't happened yet, it's been accepted. That's the
|
|
79
|
-
// entire point of enqueuing
|
|
79
|
+
// entire point of enqueuing—the response returns before the consumer runs.
|
|
80
80
|
return text("Accepted", 202);
|
|
81
81
|
}
|
|
@@ -10,12 +10,12 @@ export declare const ASTROID_REALTIME_MIGRATION_TAG = "v1";
|
|
|
10
10
|
/** Is the realtime module switched on for this project? */
|
|
11
11
|
export declare function usesRealtime(config: AstroidConfig): boolean;
|
|
12
12
|
/**
|
|
13
|
-
* `src/edit-session.ts
|
|
13
|
+
* `src/edit-session.ts`—the site-owned Durable Object subclass.
|
|
14
14
|
*
|
|
15
15
|
* Scaffold-once: `persist` is where a project decides what a flush means, and
|
|
16
16
|
* the lock/field sets are tuning. What Astroid fixes is the delegation shape,
|
|
17
17
|
* because getting it wrong fails in ways that look like anything but a bug in
|
|
18
|
-
* this file
|
|
18
|
+
* this file—a missing `webSocketClose` leaks presence forever, a non-lazy
|
|
19
19
|
* session breaks after the first hibernation wake.
|
|
20
20
|
*
|
|
21
21
|
* Returns null when the project has no realtime module.
|
|
@@ -23,8 +23,8 @@ export declare function usesRealtime(config: AstroidConfig): boolean;
|
|
|
23
23
|
export declare function generateAstroidEditSession(config: AstroidConfig): string | null;
|
|
24
24
|
/**
|
|
25
25
|
* The `CloudflareEnv` member the realtime module adds, as a block
|
|
26
|
-
* `create-astroid` substitutes into `src/env.d.ts`. Empty without the module
|
|
27
|
-
*
|
|
26
|
+
* `create-astroid` substitutes into `src/env.d.ts`. Empty without the module—a
|
|
27
|
+
* project that types a binding its wrangler.jsonc never creates is making a
|
|
28
28
|
* promise it doesn't keep.
|
|
29
29
|
*/
|
|
30
30
|
export declare function generateAstroidRealtimeEnv(config: AstroidConfig): string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The package description has claimed "multi-editor sites" since 0.1.0, and this
|
|
7
7
|
// is the half that makes it true for two people on the SAME page. (The other
|
|
8
|
-
// axis
|
|
8
|
+
// axis—multi-EDITOR, that is, an org of accounts—was always real.) Without it
|
|
9
9
|
// two editors on one page clobber each other; the server-side draft merge
|
|
10
10
|
// narrows the window but there is no live channel, no presence, and no signal
|
|
11
11
|
// that someone else is in the same field.
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
// What Astroid generates and what it deliberately does NOT:
|
|
14
14
|
//
|
|
15
15
|
// - The DO SUBCLASS is scaffold-once (`src/edit-session.ts`), because it must
|
|
16
|
-
// import `cloudflare:workers
|
|
17
|
-
// carry
|
|
16
|
+
// import `cloudflare:workers`—a runtime-only specifier the toolkit can't
|
|
17
|
+
// carry—and because its `persist` is the seam a project tunes. Louise
|
|
18
18
|
// ships the session LOGIC it delegates to; this is the boilerplate around it.
|
|
19
19
|
// - The wrangler `durable_objects` binding + `migrations` block, which is the
|
|
20
20
|
// part nobody gets right from memory: a DO class needs a migration tag, and
|
|
21
21
|
// a SQLite-backed one needs `new_sqlite_classes` rather than `new_classes`.
|
|
22
22
|
// - The `realtimeRoute` upgrade endpoint, in the generated worker.
|
|
23
23
|
//
|
|
24
|
-
// Persistence goes through `applySaveDraft
|
|
24
|
+
// Persistence goes through `applySaveDraft`—the SAME path the fetch auto-save
|
|
25
25
|
// uses. One write path, per the ADR: the DO is a new front end to it, not a
|
|
26
26
|
// parallel store, so drafts, version history, publish, and read-your-writes all
|
|
27
27
|
// stay intact.
|
|
@@ -38,12 +38,12 @@ export function usesRealtime(config) {
|
|
|
38
38
|
return (config.modules ?? []).includes("realtime");
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* `src/edit-session.ts
|
|
41
|
+
* `src/edit-session.ts`—the site-owned Durable Object subclass.
|
|
42
42
|
*
|
|
43
43
|
* Scaffold-once: `persist` is where a project decides what a flush means, and
|
|
44
44
|
* the lock/field sets are tuning. What Astroid fixes is the delegation shape,
|
|
45
45
|
* because getting it wrong fails in ways that look like anything but a bug in
|
|
46
|
-
* this file
|
|
46
|
+
* this file—a missing `webSocketClose` leaks presence forever, a non-lazy
|
|
47
47
|
* session breaks after the first hibernation wake.
|
|
48
48
|
*
|
|
49
49
|
* Returns null when the project has no realtime module.
|
|
@@ -143,8 +143,8 @@ export function generateAstroidEditSession(config) {
|
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* The `CloudflareEnv` member the realtime module adds, as a block
|
|
146
|
-
* `create-astroid` substitutes into `src/env.d.ts`. Empty without the module
|
|
147
|
-
*
|
|
146
|
+
* `create-astroid` substitutes into `src/env.d.ts`. Empty without the module—a
|
|
147
|
+
* project that types a binding its wrangler.jsonc never creates is making a
|
|
148
148
|
* promise it doesn't keep.
|
|
149
149
|
*/
|
|
150
150
|
export function generateAstroidRealtimeEnv(config) {
|
|
@@ -2,18 +2,18 @@ import { type CollectionConfig, type ContentConfig } from "louise-toolkit/conten
|
|
|
2
2
|
import type { AstroidConfig } from "../config.js";
|
|
3
3
|
/**
|
|
4
4
|
* Return a copy of a `pages` write payload with its `sections` rich-text fields
|
|
5
|
-
* sanitized against the project media base
|
|
5
|
+
* sanitized against the project media base—a no-op when the write carries no
|
|
6
6
|
* `sections`. Pure; leaves every other field (and a partial PATCH's absent ones)
|
|
7
7
|
* untouched.
|
|
8
8
|
*
|
|
9
9
|
* Exported because two write paths need it: the collection's `beforeChange` hook
|
|
10
|
-
* below, AND the raw `pagesRoute` (which does not run collection hooks
|
|
10
|
+
* below, AND the raw `pagesRoute` (which does not run collection hooks—see
|
|
11
11
|
* {@link astroidPagesWriteHooks}).
|
|
12
12
|
*/
|
|
13
13
|
export declare function sanitizeAstroidPageSections(config: AstroidConfig, data: Record<string, unknown>): Record<string, unknown>;
|
|
14
14
|
/**
|
|
15
15
|
* Validate the (already-sanitized) `sections` of a `pages` write against the
|
|
16
|
-
* catalog, throwing `LouiseValidationError
|
|
16
|
+
* catalog, throwing `LouiseValidationError`—an unknown `_type`, a field of the
|
|
17
17
|
* wrong shape, or a setting outside its declared options is rejected with a 422
|
|
18
18
|
* carrying the per-field violations. A no-op when the write carries no
|
|
19
19
|
* `sections`, so a partial PATCH of other fields isn't spuriously validated.
|
|
@@ -23,7 +23,7 @@ export declare function assertAstroidPageSections(config: AstroidConfig, data: R
|
|
|
23
23
|
* The write-time hooks the raw `pagesRoute` (louise-toolkit/editor) needs to
|
|
24
24
|
* enforce the same section contract as the draft path.
|
|
25
25
|
*
|
|
26
|
-
* `pagesRoute` writes straight to the table and
|
|
26
|
+
* `pagesRoute` writes straight to the table and—unlike `versionsRoute`—takes
|
|
27
27
|
* no collection config, so it never runs the `beforeChange` hook below. Left
|
|
28
28
|
* bare (as it was), a direct `POST` / `PATCH /api/louise/pages/:id` persists an
|
|
29
29
|
* unknown section `_type`, a setting outside its options, or unsanitized section
|
|
@@ -47,8 +47,8 @@ export declare function astroidPagesWriteHooks(config: AstroidConfig): {
|
|
|
47
47
|
export declare function astroidPagesCollection(config: AstroidConfig): CollectionConfig;
|
|
48
48
|
/**
|
|
49
49
|
* The Louise `ContentConfig` for an Astroid project. Today: the `pages`
|
|
50
|
-
* collection. Archetype- and module-specific collections (
|
|
51
|
-
* `gallery`) layer in here as they land
|
|
50
|
+
* collection. Archetype- and module-specific collections (for example, a portfolio
|
|
51
|
+
* `gallery`) layer in here as they land—this is the single place that maps
|
|
52
52
|
* brand config down to Louise content.
|
|
53
53
|
*/
|
|
54
54
|
export declare function astroidContentConfig(config: AstroidConfig): ContentConfig;
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// Astroid → Louise content mapping. This is the opinionated seam: given an
|
|
4
4
|
// Astroid project config, derive the Louise `CollectionConfig`(s) a site needs.
|
|
5
5
|
// Astroid decides WHICH collections and fields exist (its opinions); Louise's
|
|
6
|
-
// codegen decides HOW they become D1 tables. Dependency flows one way
|
|
6
|
+
// codegen decides HOW they become D1 tables. Dependency flows one way—this
|
|
7
7
|
// imports `louise-toolkit/content`, never the reverse.
|
|
8
8
|
// `content/define` and `content/sections` rather than the `content` barrel: the
|
|
9
9
|
// barrel eagerly pulls the codegen/localApi/validation chunks, and those import
|
|
10
|
-
// drizzle-orm for real
|
|
11
|
-
// here would force a package on consumers who only DESCRIBE content (
|
|
10
|
+
// drizzle-orm for real—an *optional* peer of louise-toolkit, so importing it
|
|
11
|
+
// here would force a package on consumers who only DESCRIBE content (for example,
|
|
12
12
|
// create-astroid's schema generators, which call this function but never run the
|
|
13
13
|
// beforeChange hook below). Both entries are drizzle-free: `content/define` for
|
|
14
14
|
// the config types/builders, and `content/sections` for the write-time section
|
|
@@ -18,23 +18,23 @@
|
|
|
18
18
|
// drizzle-free.
|
|
19
19
|
//
|
|
20
20
|
// Note the entry named here is the PUBLIC subpath. Astroid must never reach into
|
|
21
|
-
// `louise-toolkit/src
|
|
21
|
+
// `louise-toolkit/src/...`—that resolves only because the workspace aliases the
|
|
22
22
|
// package to its source, and would break the moment astroid consumes a published
|
|
23
23
|
// tarball (#327).
|
|
24
24
|
import { defineCollection, } from "louise-toolkit/content/define";
|
|
25
25
|
import { assertValidSections, sanitizeSectionsRichText } from "louise-toolkit/content/sections";
|
|
26
26
|
import { sanitizeRichHtml } from "louise-toolkit/security";
|
|
27
|
-
// The catalog is the single declaration of what a section IS
|
|
27
|
+
// The catalog is the single declaration of what a section IS—the same object
|
|
28
28
|
// the on-canvas editor mounts with and this hook validates against. It lives
|
|
29
29
|
// beside the components (it ships as source for them) and is imported here so
|
|
30
30
|
// the two can't drift.
|
|
31
31
|
import { astroidSectionCatalog } from "../components/sections.js";
|
|
32
32
|
/**
|
|
33
|
-
* The opinionated `pages` collection
|
|
33
|
+
* The opinionated `pages` collection—the EDITABLE page fields, versioned
|
|
34
34
|
* drafts, and full-text search. Keyed to the same names as Louise's `pagesColumns`
|
|
35
35
|
* so a publish's `.set()` maps straight onto the physical columns; bookkeeping
|
|
36
36
|
* columns (`id`/`status`/timestamps/`publishedVersionId`) live on the table via
|
|
37
|
-
* `pagesColumns`, never here
|
|
37
|
+
* `pagesColumns`, never here—matching the site's `pages-collection.ts`.
|
|
38
38
|
*
|
|
39
39
|
* Validated by `defineCollection` at build time, so a malformed field shape throws
|
|
40
40
|
* here rather than at codegen.
|
|
@@ -58,21 +58,21 @@ function resolveSectionCatalog(config) {
|
|
|
58
58
|
* declaring a `blocks` policy, no block is ever reached.
|
|
59
59
|
*
|
|
60
60
|
* It is NOT optional once a site does declare one. Unlike the section catalog
|
|
61
|
-
* there's no built-in fallback to borrow
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* there's no built-in fallback to borrow—block types are wholly site-defined—so
|
|
62
|
+
* an unset catalog means every block `_type` reads as unknown and the write
|
|
63
|
+
* returns 422, and block rich text goes unsanitized because its def can't be resolved.
|
|
64
64
|
*/
|
|
65
65
|
function resolveBlockCatalog(config) {
|
|
66
66
|
return config.blockCatalog ?? {};
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Return a copy of a `pages` write payload with its `sections` rich-text fields
|
|
70
|
-
* sanitized against the project media base
|
|
70
|
+
* sanitized against the project media base—a no-op when the write carries no
|
|
71
71
|
* `sections`. Pure; leaves every other field (and a partial PATCH's absent ones)
|
|
72
72
|
* untouched.
|
|
73
73
|
*
|
|
74
74
|
* Exported because two write paths need it: the collection's `beforeChange` hook
|
|
75
|
-
* below, AND the raw `pagesRoute` (which does not run collection hooks
|
|
75
|
+
* below, AND the raw `pagesRoute` (which does not run collection hooks—see
|
|
76
76
|
* {@link astroidPagesWriteHooks}).
|
|
77
77
|
*/
|
|
78
78
|
export function sanitizeAstroidPageSections(config, data) {
|
|
@@ -84,7 +84,7 @@ export function sanitizeAstroidPageSections(config, data) {
|
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Validate the (already-sanitized) `sections` of a `pages` write against the
|
|
87
|
-
* catalog, throwing `LouiseValidationError
|
|
87
|
+
* catalog, throwing `LouiseValidationError`—an unknown `_type`, a field of the
|
|
88
88
|
* wrong shape, or a setting outside its declared options is rejected with a 422
|
|
89
89
|
* carrying the per-field violations. A no-op when the write carries no
|
|
90
90
|
* `sections`, so a partial PATCH of other fields isn't spuriously validated.
|
|
@@ -102,7 +102,7 @@ export async function assertAstroidPageSections(config, data, operation = "updat
|
|
|
102
102
|
* The write-time hooks the raw `pagesRoute` (louise-toolkit/editor) needs to
|
|
103
103
|
* enforce the same section contract as the draft path.
|
|
104
104
|
*
|
|
105
|
-
* `pagesRoute` writes straight to the table and
|
|
105
|
+
* `pagesRoute` writes straight to the table and—unlike `versionsRoute`—takes
|
|
106
106
|
* no collection config, so it never runs the `beforeChange` hook below. Left
|
|
107
107
|
* bare (as it was), a direct `POST` / `PATCH /api/louise/pages/:id` persists an
|
|
108
108
|
* unknown section `_type`, a setting outside its options, or unsanitized section
|
|
@@ -119,12 +119,12 @@ export async function assertAstroidPageSections(config, data, operation = "updat
|
|
|
119
119
|
export function astroidPagesWriteHooks(config) {
|
|
120
120
|
const mediaBase = pageMediaBase(config);
|
|
121
121
|
return {
|
|
122
|
-
// `body` is a richField, so it goes through pagesRoute's own sanitize seam
|
|
123
|
-
//
|
|
122
|
+
// `body` is a richField, so it goes through pagesRoute's own sanitize seam—with
|
|
123
|
+
// the project media base, matching the hook rather than the toolkit
|
|
124
124
|
// default sanitizer that knows no media base.
|
|
125
125
|
sanitize: (html) => sanitizeRichHtml(html, { mediaBase }),
|
|
126
126
|
// `sections` is not a richField, so it's sanitized here in the transform,
|
|
127
|
-
// which pagesRoute runs BEFORE validate
|
|
127
|
+
// which pagesRoute runs BEFORE validate—the hook's sanitize-then-validate
|
|
128
128
|
// order.
|
|
129
129
|
transform: (data) => sanitizeAstroidPageSections(config, data),
|
|
130
130
|
validate: (data, ctx) => assertAstroidPageSections(config, data, ctx.operation),
|
|
@@ -132,14 +132,14 @@ export function astroidPagesWriteHooks(config) {
|
|
|
132
132
|
}
|
|
133
133
|
export function astroidPagesCollection(config) {
|
|
134
134
|
// The `body` is rich HTML edited in place (`<Editable type="richtext">`) and
|
|
135
|
-
// staged as a draft, so sanitize it on every write
|
|
135
|
+
// staged as a draft, so sanitize it on every write—never store raw HTML. A
|
|
136
136
|
// pasted `<img>` pointing off-origin (a hotlink) is dropped: body images must
|
|
137
137
|
// live in the media library. Mirrors the reference site's pages-collection hook.
|
|
138
138
|
const mediaBase = pageMediaBase(config);
|
|
139
139
|
const fields = {};
|
|
140
140
|
fields.slug = { type: "text", required: true };
|
|
141
141
|
fields.title = { type: "text", required: true };
|
|
142
|
-
// Sanitized rich HTML (a string), not TipTap JSON
|
|
142
|
+
// Sanitized rich HTML (a string), not TipTap JSON—matches `pagesColumns.body`.
|
|
143
143
|
fields.body = { type: "text" };
|
|
144
144
|
fields.seoTitle = { type: "text" };
|
|
145
145
|
fields.seoDescription = { type: "text" };
|
|
@@ -147,7 +147,7 @@ export function astroidPagesCollection(config) {
|
|
|
147
147
|
fields.noindex = { type: "checkbox" };
|
|
148
148
|
fields.sortOrder = { type: "number" };
|
|
149
149
|
// Structured page-builder blocks (the editable home), deep-validated against
|
|
150
|
-
// the section catalog on write
|
|
150
|
+
// the section catalog on write—see the beforeChange hook below.
|
|
151
151
|
fields.sections = { type: "json" };
|
|
152
152
|
return defineCollection({
|
|
153
153
|
slug: "pages",
|
|
@@ -163,7 +163,7 @@ export function astroidPagesCollection(config) {
|
|
|
163
163
|
// validating the raw value would pass content the sanitizer is about
|
|
164
164
|
// to change. Same order as the body above. Both steps are shared with
|
|
165
165
|
// the raw pagesRoute (see astroidPagesWriteHooks) so the two write
|
|
166
|
-
// paths can't diverge
|
|
166
|
+
// paths can't diverge—the sanitize throws nothing, the assert throws
|
|
167
167
|
// LouiseValidationError → 422 with per-field violations.
|
|
168
168
|
next = sanitizeAstroidPageSections(config, next);
|
|
169
169
|
await assertAstroidPageSections(config, next, "update");
|
|
@@ -177,8 +177,8 @@ export function astroidPagesCollection(config) {
|
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* The Louise `ContentConfig` for an Astroid project. Today: the `pages`
|
|
180
|
-
* collection. Archetype- and module-specific collections (
|
|
181
|
-
* `gallery`) layer in here as they land
|
|
180
|
+
* collection. Archetype- and module-specific collections (for example, a portfolio
|
|
181
|
+
* `gallery`) layer in here as they land—this is the single place that maps
|
|
182
182
|
* brand config down to Louise content.
|
|
183
183
|
*/
|
|
184
184
|
export function astroidContentConfig(config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AstroidConfig } from "../config.js";
|
|
2
2
|
/** A ready-made table Astroid re-exports from `louise-toolkit/db`. */
|
|
3
3
|
export type AstroidFrameworkTable = "inquiries" | "media" | "siteSettings";
|
|
4
|
-
/** True when the site captures inquiries
|
|
4
|
+
/** True when the site captures inquiries—a contact section or a
|
|
5
5
|
* wholesale-inquiry module. Shared by table selection (here) and route selection
|
|
6
6
|
* (the worker route plan). */
|
|
7
7
|
export declare function capturesInquiries(config: AstroidConfig): boolean;
|
package/dist/schema/framework.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// tables never drift from Louise. `media` and `siteSettings` are universal;
|
|
6
6
|
// `inquiries` is pulled in only when a brand actually captures inquiries (a
|
|
7
7
|
// contact section, or a wholesale-inquiry module).
|
|
8
|
-
/** True when the site captures inquiries
|
|
8
|
+
/** True when the site captures inquiries—a contact section or a
|
|
9
9
|
* wholesale-inquiry module. Shared by table selection (here) and route selection
|
|
10
10
|
* (the worker route plan). */
|
|
11
11
|
export function capturesInquiries(config) {
|
|
12
|
-
// Explicit override wins
|
|
12
|
+
// Explicit override wins—a site whose inquiry surface is a bespoke section
|
|
13
13
|
// (coracle's custom `contactForm`) can't be detected from the built-in
|
|
14
14
|
// vocabulary, so it says so directly.
|
|
15
15
|
if (typeof config.inquiries === "boolean")
|
package/dist/schema/generate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// generateAstroidSchema
|
|
3
|
+
// generateAstroidSchema—emit the Drizzle schema source (a site's `schema.ts`)
|
|
4
4
|
// from an Astroid config. This is the boilerplate every Louise site hand-writes:
|
|
5
5
|
// compose the framework `pagesColumns` into a `pages` table, add the versions
|
|
6
6
|
// table + publish pointer, and re-export the ready-made framework tables. Astroid
|
|
@@ -24,7 +24,7 @@ export function generateAstroidSchema(config) {
|
|
|
24
24
|
// One import brings them all in (matching the site) so nothing is unused.
|
|
25
25
|
const dbImports = [...framework, "pagesColumns"].sort();
|
|
26
26
|
const catalog = generateCatalogTable(config);
|
|
27
|
-
// Only import the column builders the emitted source actually uses
|
|
27
|
+
// Only import the column builders the emitted source actually uses—an
|
|
28
28
|
// unused import is a lint error in the project we're generating into, and a
|
|
29
29
|
// missing one (`real`, from the catalog's price/sortOrder) is a type error.
|
|
30
30
|
const drizzleImports = ["integer", "sqliteTable", "text"];
|
package/dist/schema/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// Schema generation
|
|
3
|
+
// Schema generation—config → Louise content config → Drizzle schema source.
|
|
4
4
|
export * from "./collections.js";
|
|
5
5
|
export * from "./framework.js";
|
|
6
6
|
export * from "./generate.js";
|
package/dist/secrets.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type SecretSource } from "louise-toolkit/security";
|
|
|
2
2
|
export type { SecretSource };
|
|
3
3
|
/**
|
|
4
4
|
* The placeholder every Astroid scaffold seeds its unprovisioned secrets with.
|
|
5
|
-
* Reading it back means "not configured yet", never a credential
|
|
5
|
+
* Reading it back means "not configured yet", never a credential—the value is
|
|
6
6
|
* deliberately loud so it is obvious in a Secrets Store listing or a log line.
|
|
7
7
|
*
|
|
8
8
|
* It matches the sentinel `louise-toolkit`'s Turnstile gate already recognizes,
|
|
@@ -11,7 +11,7 @@ export type { SecretSource };
|
|
|
11
11
|
export declare const ASTROID_SECRET_PLACEHOLDER = "DUMMY_REPLACE_ME";
|
|
12
12
|
/**
|
|
13
13
|
* Read one secret under the Astroid convention: `null` unless it holds a real,
|
|
14
|
-
* non-placeholder value. Thin by design
|
|
14
|
+
* non-placeholder value. Thin by design—the reason to call this rather than
|
|
15
15
|
* `readSecret` directly is that it binds Astroid's sentinel for you.
|
|
16
16
|
*/
|
|
17
17
|
export declare function readModuleSecret(source: SecretSource): Promise<string | null>;
|
|
@@ -19,14 +19,14 @@ export declare function readModuleSecret(source: SecretSource): Promise<string |
|
|
|
19
19
|
export interface ModuleSecrets<K extends string> {
|
|
20
20
|
/**
|
|
21
21
|
* True when EVERY secret the module declared resolved to a real value. The
|
|
22
|
-
* module's `isConfigured()` should be exactly this
|
|
22
|
+
* module's `isConfigured()` should be exactly this—partial provisioning is
|
|
23
23
|
* treated as dormant, since a half-configured integration fails at the worst
|
|
24
24
|
* possible moment (mid-checkout) rather than at boot.
|
|
25
25
|
*/
|
|
26
26
|
configured: boolean;
|
|
27
27
|
/** Each declared secret's resolved value, or `null` where unprovisioned. */
|
|
28
28
|
values: Record<K, string | null>;
|
|
29
|
-
/** The still-unprovisioned names, in declaration order
|
|
29
|
+
/** The still-unprovisioned names, in declaration order—the "why not" list. */
|
|
30
30
|
missing: K[];
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
@@ -46,9 +46,9 @@ export interface ModuleSecrets<K extends string> {
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function resolveModuleSecrets<K extends string>(sources: Record<K, SecretSource>): Promise<ModuleSecrets<K>>;
|
|
48
48
|
/**
|
|
49
|
-
* A one-line, human-readable status for a module
|
|
49
|
+
* A one-line, human-readable status for a module—what `astroid doctor`, a dev
|
|
50
50
|
* server banner, or a health endpoint should print. Naming the missing secrets
|
|
51
51
|
* is the whole point: "commerce is off" sends someone reading source, "commerce
|
|
52
|
-
* is dormant
|
|
52
|
+
* is dormant—set SQUARE_ACCESS_TOKEN" does not.
|
|
53
53
|
*/
|
|
54
54
|
export declare function describeModuleStatus<K extends string>(module: string, status: ModuleSecrets<K>): string;
|
package/dist/secrets.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Astroid's optional modules are opt-in at the CONFIG level but not at the
|
|
6
6
|
// account level: switching `commerce` on in `defineAstroid` must not require a
|
|
7
7
|
// Square account before `pnpm dev` will boot. So every module here follows one
|
|
8
|
-
// rule
|
|
8
|
+
// rule—a module whose secrets are unprovisioned is DORMANT: it renders, it
|
|
9
9
|
// serves, it says out loud that it is simulated, and it never calls upstream
|
|
10
10
|
// with a dummy credential.
|
|
11
11
|
//
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// absent / unreadable / empty / a placeholder reads as `null`). What lives here
|
|
14
14
|
// is the *convention* over it, which is Astroid's opinion, not the toolkit's:
|
|
15
15
|
//
|
|
16
|
-
// 1. ONE sentinel
|
|
16
|
+
// 1. ONE sentinel—`ASTROID_SECRET_PLACEHOLDER`—seeded by `create-astroid`
|
|
17
17
|
// into every secret a scaffold declares, so a fresh clone has a complete,
|
|
18
18
|
// valid binding set and zero real credentials.
|
|
19
19
|
// 2. `resolveModuleSecrets` collapses a module's whole secret set into one
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { readSecret } from "louise-toolkit/security";
|
|
25
25
|
/**
|
|
26
26
|
* The placeholder every Astroid scaffold seeds its unprovisioned secrets with.
|
|
27
|
-
* Reading it back means "not configured yet", never a credential
|
|
27
|
+
* Reading it back means "not configured yet", never a credential—the value is
|
|
28
28
|
* deliberately loud so it is obvious in a Secrets Store listing or a log line.
|
|
29
29
|
*
|
|
30
30
|
* It matches the sentinel `louise-toolkit`'s Turnstile gate already recognizes,
|
|
@@ -33,7 +33,7 @@ import { readSecret } from "louise-toolkit/security";
|
|
|
33
33
|
export const ASTROID_SECRET_PLACEHOLDER = "DUMMY_REPLACE_ME";
|
|
34
34
|
/**
|
|
35
35
|
* Read one secret under the Astroid convention: `null` unless it holds a real,
|
|
36
|
-
* non-placeholder value. Thin by design
|
|
36
|
+
* non-placeholder value. Thin by design—the reason to call this rather than
|
|
37
37
|
* `readSecret` directly is that it binds Astroid's sentinel for you.
|
|
38
38
|
*/
|
|
39
39
|
export function readModuleSecret(source) {
|
|
@@ -68,10 +68,10 @@ export async function resolveModuleSecrets(sources) {
|
|
|
68
68
|
return { configured: missing.length === 0, values, missing };
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* A one-line, human-readable status for a module
|
|
71
|
+
* A one-line, human-readable status for a module—what `astroid doctor`, a dev
|
|
72
72
|
* server banner, or a health endpoint should print. Naming the missing secrets
|
|
73
73
|
* is the whole point: "commerce is off" sends someone reading source, "commerce
|
|
74
|
-
* is dormant
|
|
74
|
+
* is dormant—set SQUARE_ACCESS_TOKEN" does not.
|
|
75
75
|
*/
|
|
76
76
|
export function describeModuleStatus(module, status) {
|
|
77
77
|
if (status.configured)
|
|
@@ -11,6 +11,6 @@ export declare function astroidCspOrigins(config: AstroidConfig): Required<CspOr
|
|
|
11
11
|
* `'unsafe-inline'` is required (Louise's data-driven `style=""` carriers and
|
|
12
12
|
* the editor's runtime `<style>`), which is why Astro can't own this directive:
|
|
13
13
|
* per spec a single hash here voids `'unsafe-inline'`. Module and config
|
|
14
|
-
* `style` origins ride along,
|
|
14
|
+
* `style` origins ride along, for example, the Square SDK's own stylesheet.
|
|
15
15
|
*/
|
|
16
16
|
export declare function astroidCspStyleSrc(config: AstroidConfig): string;
|
|
@@ -31,11 +31,11 @@ const COMMERCE_ORIGINS = {
|
|
|
31
31
|
};
|
|
32
32
|
// Turnstile. Always allowed, not gated on the captcha being configured: the
|
|
33
33
|
// scaffold ships the widget dormant (see the dormant-until-provisioned
|
|
34
|
-
// convention) and it must not need a rebuild to switch on
|
|
34
|
+
// convention) and it must not need a rebuild to switch on—CSP is baked at
|
|
35
35
|
// build time, the secret is a runtime value.
|
|
36
36
|
const TURNSTILE = turnstileCsp();
|
|
37
37
|
// The map module. MapLibre spins its tile-decoding workers up from blob: URLs,
|
|
38
|
-
// so `worker-src blob:` is not optional
|
|
38
|
+
// so `worker-src blob:` is not optional—without it the map renders an empty
|
|
39
39
|
// canvas and the console fills with worker-construction errors.
|
|
40
40
|
//
|
|
41
41
|
// Nothing else is needed, and that's the whole argument for the self-hosted
|
|
@@ -69,7 +69,7 @@ export function astroidCspOrigins(config) {
|
|
|
69
69
|
* `'unsafe-inline'` is required (Louise's data-driven `style=""` carriers and
|
|
70
70
|
* the editor's runtime `<style>`), which is why Astro can't own this directive:
|
|
71
71
|
* per spec a single hash here voids `'unsafe-inline'`. Module and config
|
|
72
|
-
* `style` origins ride along,
|
|
72
|
+
* `style` origins ride along, for example, the Square SDK's own stylesheet.
|
|
73
73
|
*/
|
|
74
74
|
export function astroidCspStyleSrc(config) {
|
|
75
75
|
return ["'self'", "'unsafe-inline'", ...astroidCspOrigins(config).style].join(" ");
|
|
@@ -2,7 +2,7 @@ import type { RateRule } from "louise-toolkit/security";
|
|
|
2
2
|
import type { AstroidConfig } from "../config.js";
|
|
3
3
|
export type { RateRule };
|
|
4
4
|
/**
|
|
5
|
-
* Base path the customer/portal Better Auth instance mounts at
|
|
5
|
+
* Base path the customer/portal Better Auth instance mounts at—its own
|
|
6
6
|
* handler, separate from the editor's `/api/auth`. Fixed by Astroid so the rate
|
|
7
7
|
* rules, the middleware, and the portal routes can't drift apart.
|
|
8
8
|
*/
|
|
@@ -15,7 +15,7 @@ export declare const ASTROID_CHECKOUT_PATH = "/api/checkout";
|
|
|
15
15
|
* checkout when commerce is configured.
|
|
16
16
|
*
|
|
17
17
|
* Rules are matched first-wins, and `security.rateRules` from the config are
|
|
18
|
-
* placed FIRST
|
|
18
|
+
* placed FIRST—so a site tightens or loosens any default by declaring its own
|
|
19
19
|
* rule for that path, rather than losing the whole set to override one budget.
|
|
20
20
|
*/
|
|
21
21
|
export declare function astroidRateRules(config: AstroidConfig): RateRule[];
|
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
// The limiter mechanism lives in `louise-toolkit/security` and is deliberately
|
|
6
6
|
// unopinionated: which routes, and which budgets, are policy. But the policy
|
|
7
7
|
// turned out not to vary. All three consuming sites independently wrote the same
|
|
8
|
-
// `RateRule[]
|
|
9
|
-
// budgets within a factor of one of each other
|
|
8
|
+
// `RateRule[]`—the same public POST surfaces, the same 10-minute windows,
|
|
9
|
+
// budgets within a factor of one of each other—differing only where the site
|
|
10
10
|
// had a surface the others didn't (a portal, a checkout). That is a default, not
|
|
11
11
|
// a per-site decision, so Astroid derives the whole set from the config.
|
|
12
12
|
//
|
|
13
13
|
// What's in scope: the public, UNAUTHENTICATED POST surfaces. Editor endpoints
|
|
14
|
-
// (`/api/louise/*`) are session-gated and stay out on purpose
|
|
14
|
+
// (`/api/louise/*`) are session-gated and stay out on purpose—a limiter that
|
|
15
15
|
// can lock the owner out of their own studio is worse than the abuse it stops.
|
|
16
16
|
// The contact form is also absent by design: it's a worker route with its own
|
|
17
17
|
// per-form limiter, and worker routes are matched before Astro's middleware ever
|
|
18
18
|
// runs, so a rule here would never fire.
|
|
19
19
|
/**
|
|
20
|
-
* Base path the customer/portal Better Auth instance mounts at
|
|
20
|
+
* Base path the customer/portal Better Auth instance mounts at—its own
|
|
21
21
|
* handler, separate from the editor's `/api/auth`. Fixed by Astroid so the rate
|
|
22
22
|
* rules, the middleware, and the portal routes can't drift apart.
|
|
23
23
|
*/
|
|
24
24
|
export const ASTROID_PORTAL_BASE_PATH = "/api/portal-auth";
|
|
25
25
|
/** Path the commerce module's checkout POSTs to. */
|
|
26
26
|
export const ASTROID_CHECKOUT_PATH = "/api/checkout";
|
|
27
|
-
/** Ten minutes. Every default budget uses this window
|
|
27
|
+
/** Ten minutes. Every default budget uses this window—long enough that a
|
|
28
28
|
* burst can't wait it out, short enough that a false positive self-heals. */
|
|
29
29
|
const WINDOW = 600;
|
|
30
30
|
const exact = (path) => (p) => p === path;
|
|
@@ -34,7 +34,7 @@ const exact = (path) => (p) => p === path;
|
|
|
34
34
|
* checkout when commerce is configured.
|
|
35
35
|
*
|
|
36
36
|
* Rules are matched first-wins, and `security.rateRules` from the config are
|
|
37
|
-
* placed FIRST
|
|
37
|
+
* placed FIRST—so a site tightens or loosens any default by declaring its own
|
|
38
38
|
* rule for that path, rather than losing the whole set to override one budget.
|
|
39
39
|
*/
|
|
40
40
|
export function astroidRateRules(config) {
|
|
@@ -62,9 +62,9 @@ export function astroidRateRules(config) {
|
|
|
62
62
|
if (config.portal?.enabled) {
|
|
63
63
|
// The portal's mount is configurable (a site may already ship a second
|
|
64
64
|
// instance at its own path), so the credential-surface rules must track the
|
|
65
|
-
// resolved base path, not the default constant
|
|
65
|
+
// resolved base path, not the default constant—mirrors `astroidPortal`.
|
|
66
66
|
const base = config.portal.basePath ?? ASTROID_PORTAL_BASE_PATH;
|
|
67
|
-
// Customer credentials, unlike the editor's, are password-based
|
|
67
|
+
// Customer credentials, unlike the editor's, are password-based—so these
|
|
68
68
|
// guard credential stuffing and enumeration, not just mail volume.
|
|
69
69
|
rules.push({
|
|
70
70
|
name: "portal-signup",
|
package/dist/seo/resolve.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface AstroidSeoSettings {
|
|
|
8
8
|
tagline?: string | null;
|
|
9
9
|
metaDescription?: string | null;
|
|
10
10
|
defaultOgImageUrl?: string | null;
|
|
11
|
-
/** Site-wide kill switch
|
|
11
|
+
/** Site-wide kill switch—noindex every page (staging, pre-launch). */
|
|
12
12
|
disableIndexing?: boolean | null;
|
|
13
13
|
}
|
|
14
14
|
/** Per-page SEO: an editor's overrides, or a built-in page's own defaults. */
|
|
@@ -24,7 +24,7 @@ export interface PageSeoInput {
|
|
|
24
24
|
noindex?: boolean | null;
|
|
25
25
|
}
|
|
26
26
|
export interface AstroidSeoOptions {
|
|
27
|
-
/** Canonical URL of the page being rendered
|
|
27
|
+
/** Canonical URL of the page being rendered—absolute. */
|
|
28
28
|
canonical: string;
|
|
29
29
|
/**
|
|
30
30
|
* Title template, `%s` standing in for the page title. Applied ONLY when the
|
|
@@ -33,15 +33,15 @@ export interface AstroidSeoOptions {
|
|
|
33
33
|
titleTemplate?: string;
|
|
34
34
|
/** `@<handle>` for Twitter/X card attribution. */
|
|
35
35
|
twitterHandle?: string;
|
|
36
|
-
/** OG locale,
|
|
36
|
+
/** OG locale, for example, `"en_US"`. */
|
|
37
37
|
locale?: string;
|
|
38
38
|
}
|
|
39
39
|
/** Everything a `<head>` needs, fully resolved and absolute. */
|
|
40
40
|
export interface ResolvedSeo {
|
|
41
|
-
/** Final `<title
|
|
41
|
+
/** Final `<title>`—templated when the page supplied one. */
|
|
42
42
|
title: string;
|
|
43
43
|
/** Untemplated page title, for OG/Twitter (which shouldn't carry the site
|
|
44
|
-
* suffix twice
|
|
44
|
+
* suffix twice—the OG `site_name` already says it). */
|
|
45
45
|
bareTitle: string;
|
|
46
46
|
description?: string;
|
|
47
47
|
canonical: string;
|