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.
- package/README.md +42 -42
- package/bin/astroid.mjs +35 -29
- package/dist/analytics/index.d.ts +3 -3
- package/dist/analytics/index.js +11 -11
- 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 +27 -27
- 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 +12 -12
- 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 +89 -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 +8 -8
- package/dist/map/style.d.ts +4 -4
- package/dist/map/style.js +1 -1
- package/dist/portal/config.d.ts +3 -3
- package/dist/portal/config.js +11 -10
- 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 +13 -13
- package/dist/portal/session.d.ts +11 -5
- package/dist/portal/session.js +10 -5
- package/dist/portfolio/scaffold.d.ts +1 -1
- package/dist/portfolio/scaffold.js +8 -8
- package/dist/project/actions.d.ts +1 -1
- package/dist/project/actions.js +18 -12
- package/dist/project/generate.d.ts +4 -4
- package/dist/project/generate.js +26 -26
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +2 -1
- package/dist/project/scaffold.d.ts +2 -2
- package/dist/project/scaffold.js +69 -13
- package/dist/project/seed.d.ts +22 -0
- package/dist/project/seed.js +214 -0
- package/dist/pwa/generate.d.ts +11 -11
- package/dist/pwa/generate.js +19 -19
- 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 +17 -17
- 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 +12 -12
- package/dist/schema/collections.d.ts +42 -11
- package/dist/schema/collections.js +43 -42
- package/dist/schema/framework.d.ts +1 -1
- package/dist/schema/framework.js +2 -2
- package/dist/schema/generate.js +6 -6
- 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 +11 -11
- package/dist/worker/generate.d.ts +2 -2
- package/dist/worker/generate.js +91 -57
- package/dist/worker/index.js +1 -1
- package/dist/worker/routes.js +11 -11
- 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 +11 -11
- package/package.json +3 -4
- 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
|
@@ -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.
|
|
@@ -99,47 +99,48 @@ export async function assertAstroidPageSections(config, data, operation = "updat
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* no collection config, so it never runs the `beforeChange` hook below. Left
|
|
107
|
-
* bare (as it was), a direct `POST` / `PATCH /api/louise/pages/:id` persists an
|
|
108
|
-
* unknown section `_type`, a setting outside its options, or unsanitized section
|
|
109
|
-
* rich text: exactly what the hook exists to stop, silently missing from the one
|
|
110
|
-
* route the on-canvas *structural* edits flow through. `<Sections>` then skips
|
|
111
|
-
* the bad `_type`, so the section just vanishes with no error anywhere.
|
|
112
|
-
*
|
|
113
|
-
* These wire the SAME sanitize + validate the hook uses into `pagesRoute`'s
|
|
114
|
-
* `sanitize` / `transform` / `validate` seams, so both write paths enforce one
|
|
115
|
-
* contract. Spread into the route config:
|
|
116
|
-
*
|
|
117
|
-
* pagesRoute({ table: pages, resolveEditor, fields, ...astroidPagesWriteHooks(config) })
|
|
102
|
+
* Slugs no page may take, on any site. Each one is a path Astro, Cloudflare,
|
|
103
|
+
* or Astroid serves before the catch-all page route, so a page saved under it
|
|
104
|
+
* would be unreachable, and nothing would say why. `pagesRoute` refuses them
|
|
105
|
+
* with a 422 instead.
|
|
118
106
|
*/
|
|
119
|
-
export
|
|
107
|
+
export const ASTROID_RESERVED_SLUGS = [
|
|
108
|
+
"404",
|
|
109
|
+
"_astro",
|
|
110
|
+
"api",
|
|
111
|
+
"cdn-cgi",
|
|
112
|
+
"robots.txt",
|
|
113
|
+
"sitemap.xml",
|
|
114
|
+
];
|
|
115
|
+
export function astroidPagesWriteHooks(config, site = {}) {
|
|
120
116
|
const mediaBase = pageMediaBase(config);
|
|
121
117
|
return {
|
|
122
|
-
// `body` is a richField, so it goes through pagesRoute's own sanitize seam
|
|
123
|
-
//
|
|
118
|
+
// `body` is a richField, so it goes through pagesRoute's own sanitize seam—with
|
|
119
|
+
// the project media base, matching the hook rather than the toolkit
|
|
124
120
|
// default sanitizer that knows no media base.
|
|
125
121
|
sanitize: (html) => sanitizeRichHtml(html, { mediaBase }),
|
|
126
122
|
// `sections` is not a richField, so it's sanitized here in the transform,
|
|
127
|
-
// which pagesRoute runs BEFORE validate
|
|
128
|
-
// order.
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
// which pagesRoute runs BEFORE validate—the hook's sanitize-then-validate
|
|
124
|
+
// order. The site's own transform runs first, so Astroid's sanitize sees
|
|
125
|
+
// the value the site is about to store.
|
|
126
|
+
transform: async (data, ctx) => sanitizeAstroidPageSections(config, site.transform ? await site.transform(data, ctx) : data),
|
|
127
|
+
validate: async (data, ctx) => {
|
|
128
|
+
await site.validate?.(data, ctx);
|
|
129
|
+
await assertAstroidPageSections(config, data, ctx.operation);
|
|
130
|
+
},
|
|
131
|
+
reservedSlugs: [...ASTROID_RESERVED_SLUGS, ...(site.reservedSlugs ?? [])],
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
export function astroidPagesCollection(config) {
|
|
134
135
|
// The `body` is rich HTML edited in place (`<Editable type="richtext">`) and
|
|
135
|
-
// staged as a draft, so sanitize it on every write
|
|
136
|
+
// staged as a draft, so sanitize it on every write—never store raw HTML. A
|
|
136
137
|
// pasted `<img>` pointing off-origin (a hotlink) is dropped: body images must
|
|
137
138
|
// live in the media library. Mirrors the reference site's pages-collection hook.
|
|
138
139
|
const mediaBase = pageMediaBase(config);
|
|
139
140
|
const fields = {};
|
|
140
141
|
fields.slug = { type: "text", required: true };
|
|
141
142
|
fields.title = { type: "text", required: true };
|
|
142
|
-
// Sanitized rich HTML (a string), not TipTap JSON
|
|
143
|
+
// Sanitized rich HTML (a string), not TipTap JSON—matches `pagesColumns.body`.
|
|
143
144
|
fields.body = { type: "text" };
|
|
144
145
|
fields.seoTitle = { type: "text" };
|
|
145
146
|
fields.seoDescription = { type: "text" };
|
|
@@ -147,7 +148,7 @@ export function astroidPagesCollection(config) {
|
|
|
147
148
|
fields.noindex = { type: "checkbox" };
|
|
148
149
|
fields.sortOrder = { type: "number" };
|
|
149
150
|
// Structured page-builder blocks (the editable home), deep-validated against
|
|
150
|
-
// the section catalog on write
|
|
151
|
+
// the section catalog on write—see the beforeChange hook below.
|
|
151
152
|
fields.sections = { type: "json" };
|
|
152
153
|
return defineCollection({
|
|
153
154
|
slug: "pages",
|
|
@@ -163,7 +164,7 @@ export function astroidPagesCollection(config) {
|
|
|
163
164
|
// validating the raw value would pass content the sanitizer is about
|
|
164
165
|
// to change. Same order as the body above. Both steps are shared with
|
|
165
166
|
// the raw pagesRoute (see astroidPagesWriteHooks) so the two write
|
|
166
|
-
// paths can't diverge
|
|
167
|
+
// paths can't diverge—the sanitize throws nothing, the assert throws
|
|
167
168
|
// LouiseValidationError → 422 with per-field violations.
|
|
168
169
|
next = sanitizeAstroidPageSections(config, next);
|
|
169
170
|
await assertAstroidPageSections(config, next, "update");
|
|
@@ -177,8 +178,8 @@ export function astroidPagesCollection(config) {
|
|
|
177
178
|
}
|
|
178
179
|
/**
|
|
179
180
|
* 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
|
|
181
|
+
* collection. Archetype- and module-specific collections (for example, a portfolio
|
|
182
|
+
* `gallery`) layer in here as they land—this is the single place that maps
|
|
182
183
|
* brand config down to Louise content.
|
|
183
184
|
*/
|
|
184
185
|
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,14 +24,14 @@ 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"];
|
|
31
31
|
if (catalog?.includes("real("))
|
|
32
32
|
drizzleImports.push("real");
|
|
33
33
|
return [
|
|
34
|
-
"// Generated by astroidjs
|
|
34
|
+
"// Generated by astroidjs—do not hand-edit.",
|
|
35
35
|
"// Source: your defineAstroid config.",
|
|
36
36
|
`import { ${drizzleImports.sort().join(", ")} } from "drizzle-orm/sqlite-core";`,
|
|
37
37
|
'import { collectionVersionsTable, type JsonValue } from "louise-toolkit/content";',
|
|
@@ -46,8 +46,8 @@ export function generateAstroidSchema(config) {
|
|
|
46
46
|
' publishedVersionId: integer("published_version_id"),',
|
|
47
47
|
"});",
|
|
48
48
|
"",
|
|
49
|
-
"// Version snapshots for pages. Field-independent
|
|
50
|
-
"// JSON blob
|
|
49
|
+
"// Version snapshots for pages. Field-independent—each saved version is one",
|
|
50
|
+
"// JSON blob—so the table only needs the slug.",
|
|
51
51
|
'export const pagesVersions = collectionVersionsTable({ slug: "pages", fields: {} });',
|
|
52
52
|
"",
|
|
53
53
|
...(catalog ? [catalog] : []),
|
|
@@ -56,7 +56,7 @@ export function generateAstroidSchema(config) {
|
|
|
56
56
|
"// Site-owned tables (the ones Astroid doesn't manage): a project declares",
|
|
57
57
|
"// its own Drizzle tables in src/schema.site.ts and they're re-exported here",
|
|
58
58
|
"// so drizzle-kit sees them and the generated worker can import them. The file",
|
|
59
|
-
"// is scaffold-once
|
|
59
|
+
"// is scaffold-once—empty until a project adds a table—so this re-export is",
|
|
60
60
|
"// always safe.",
|
|
61
61
|
'export * from "./schema.site.js";',
|
|
62
62
|
"",
|
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;
|
package/dist/seo/resolve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// SEO resolution
|
|
3
|
+
// SEO resolution—settings defaults + per-page overrides, collapsed into the
|
|
4
4
|
// exact set of values a `<head>` needs.
|
|
5
5
|
//
|
|
6
6
|
// Both sites that hand-built this layer converged on the same three-level
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Kept as a pure function rather than baked into the component so it can be
|
|
15
15
|
// unit-tested, and so a route that isn't rendering a page (an OG-image endpoint,
|
|
16
16
|
// a feed) can resolve the same values.
|
|
17
|
-
/** Trimmed value, or undefined
|
|
17
|
+
/** Trimmed value, or undefined—an empty/whitespace string counts as unset. */
|
|
18
18
|
const clean = (value) => {
|
|
19
19
|
const trimmed = value?.trim();
|
|
20
20
|
return trimmed ? trimmed : undefined;
|
package/dist/seo/routes.d.ts
CHANGED
|
@@ -4,14 +4,14 @@ import type { AstroidConfig } from "../config.js";
|
|
|
4
4
|
* API always, plus the portal's account + auth surfaces and checkout when those
|
|
5
5
|
* are enabled.
|
|
6
6
|
*
|
|
7
|
-
* These are *prefixes
|
|
7
|
+
* These are *prefixes*—`robots.txt` matches by prefix, so `/api/` covers every
|
|
8
8
|
* endpoint beneath it.
|
|
9
9
|
*/
|
|
10
10
|
export declare function astroidNoindexPaths(config: AstroidConfig): string[];
|
|
11
11
|
export interface RobotsOptions {
|
|
12
|
-
/** Serving origin,
|
|
12
|
+
/** Serving origin, for example, `new URL(request.url).origin`. */
|
|
13
13
|
origin: string;
|
|
14
|
-
/** Paths to disallow
|
|
14
|
+
/** Paths to disallow—defaults to {@link astroidNoindexPaths}. */
|
|
15
15
|
disallow?: string[];
|
|
16
16
|
/**
|
|
17
17
|
* Disallow the entire site. Pass `settings.disableIndexing` so the same
|
|
@@ -25,7 +25,7 @@ export declare function astroidRobotsTxt(config: AstroidConfig, options: RobotsO
|
|
|
25
25
|
export interface SitemapEntry {
|
|
26
26
|
/** Site-root-relative path (`"/shop/beans"`) or an absolute URL. */
|
|
27
27
|
path: string;
|
|
28
|
-
/** Last modified
|
|
28
|
+
/** Last modified—a Date or an ISO string. */
|
|
29
29
|
lastmod?: Date | string;
|
|
30
30
|
}
|
|
31
31
|
export interface SitemapOptions {
|
|
@@ -38,7 +38,7 @@ export interface SitemapOptions {
|
|
|
38
38
|
*
|
|
39
39
|
* Entries are de-duplicated and sorted (a stable document diffs cleanly and
|
|
40
40
|
* caches predictably), excluded paths are dropped by prefix, and every `loc` is
|
|
41
|
-
* XML-escaped
|
|
41
|
+
* XML-escaped—a slug containing `&` would otherwise produce a malformed
|
|
42
42
|
* document that search engines reject wholesale.
|
|
43
43
|
*/
|
|
44
44
|
export declare function astroidSitemapXml(config: AstroidConfig, entries: (SitemapEntry | string)[], options: SitemapOptions): string;
|
package/dist/seo/routes.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ASTROID_PORTAL_BASE_PATH } from "../security/rate-rules.js";
|
|
|
18
18
|
* API always, plus the portal's account + auth surfaces and checkout when those
|
|
19
19
|
* are enabled.
|
|
20
20
|
*
|
|
21
|
-
* These are *prefixes
|
|
21
|
+
* These are *prefixes*—`robots.txt` matches by prefix, so `/api/` covers every
|
|
22
22
|
* endpoint beneath it.
|
|
23
23
|
*/
|
|
24
24
|
export function astroidNoindexPaths(config) {
|
|
@@ -32,7 +32,7 @@ export function astroidNoindexPaths(config) {
|
|
|
32
32
|
paths.push(ASTROID_PORTAL_BASE_PATH, "/account", "/login", "/register", "/reset-password");
|
|
33
33
|
}
|
|
34
34
|
if (config.commerce) {
|
|
35
|
-
// The checkout PAGE, not `ASTROID_CHECKOUT_PATH
|
|
35
|
+
// The checkout PAGE, not `ASTROID_CHECKOUT_PATH`—that's the POST endpoint,
|
|
36
36
|
// already covered by the `/api/` prefix. What a crawler would actually reach
|
|
37
37
|
// is the UI route.
|
|
38
38
|
paths.push("/checkout", "/cart");
|
|
@@ -68,7 +68,7 @@ const escapeXml = (value) => value.replace(/[&<>"']/g, (c) => XML_ESCAPES[c] ??
|
|
|
68
68
|
*
|
|
69
69
|
* Entries are de-duplicated and sorted (a stable document diffs cleanly and
|
|
70
70
|
* caches predictably), excluded paths are dropped by prefix, and every `loc` is
|
|
71
|
-
* XML-escaped
|
|
71
|
+
* XML-escaped—a slug containing `&` would otherwise produce a malformed
|
|
72
72
|
* document that search engines reject wholesale.
|
|
73
73
|
*/
|
|
74
74
|
export function astroidSitemapXml(config, entries, options) {
|
|
@@ -6,7 +6,7 @@ export type JsonLdNode = Record<string, unknown>;
|
|
|
6
6
|
* The schema.org `@type` each archetype describes its owner with. These are
|
|
7
7
|
* intentionally the broad parent types: pick a subtype (`CafeOrCoffeeShop`,
|
|
8
8
|
* `ArtGallery`, `HomeAndConstructionBusiness`) via `seo.businessType` when you
|
|
9
|
-
* know one
|
|
9
|
+
* know one—a more specific type is strictly better for rich results.
|
|
10
10
|
*/
|
|
11
11
|
export declare const ARCHETYPE_BUSINESS_TYPE: Record<Archetype, string>;
|
|
12
12
|
export interface StructuredDataInput {
|
|
@@ -22,7 +22,7 @@ export interface StructuredDataInput {
|
|
|
22
22
|
/** Absolute origin serving this page (the canonical host). */
|
|
23
23
|
siteUrl: string;
|
|
24
24
|
/**
|
|
25
|
-
* An extra node for the thing this page is *about
|
|
25
|
+
* An extra node for the thing this page is *about*—a Product, a
|
|
26
26
|
* VisualArtwork, an Article. Joined into the same `@graph` so crawlers see
|
|
27
27
|
* one connected description rather than three unrelated blobs.
|
|
28
28
|
*/
|
|
@@ -32,8 +32,8 @@ export interface StructuredDataInput {
|
|
|
32
32
|
* Build the JSON-LD `@graph` for a page: the business node, a `WebSite` node,
|
|
33
33
|
* and the page's own entity when there is one.
|
|
34
34
|
*
|
|
35
|
-
* The business gets a stable `@id` (`<origin>/#business`) so other nodes
|
|
36
|
-
* product's `seller`, a future `Article` author
|
|
35
|
+
* The business gets a stable `@id` (`<origin>/#business`) so other nodes—a
|
|
36
|
+
* product's `seller`, a future `Article` author—can reference it by id
|
|
37
37
|
* instead of restating it.
|
|
38
38
|
*/
|
|
39
39
|
export declare function astroidStructuredData(input: StructuredDataInput): JsonLdNode;
|
|
@@ -42,8 +42,8 @@ export declare function astroidStructuredData(input: StructuredDataInput): JsonL
|
|
|
42
42
|
*
|
|
43
43
|
* `application/ld+json` is data, not executable script, so `script-src` doesn't
|
|
44
44
|
* govern it and no CSP hash is needed. But `JSON.stringify` does **not** escape
|
|
45
|
-
* `<`, so any value folded into the graph that contains a literal `</script
|
|
46
|
-
*
|
|
45
|
+
* `<`, so any value folded into the graph that contains a literal `</script>`—a
|
|
46
|
+
* product description, an artist statement, anything editor-authored—would
|
|
47
47
|
* close the tag early and inject markup straight into `<head>`. Escaping the
|
|
48
48
|
* HTML-significant characters as `\uXXXX` keeps the payload valid JSON while
|
|
49
49
|
* making it impossible to break out of the element.
|