create-ailk 0.29.1 → 0.31.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 +71 -3
- package/component-catalog.json +524 -8
- package/component-catalog.md +8 -5
- package/dist/add-route.js +1 -0
- package/dist/cli.js +114 -1
- package/dist/cutover.d.ts +109 -0
- package/dist/cutover.js +494 -0
- package/dist/design-presets.js +20 -4
- package/dist/export-content.d.ts +55 -0
- package/dist/export-content.js +303 -0
- package/dist/inventory.d.ts +216 -0
- package/dist/inventory.js +833 -0
- package/dist/language-import.d.ts +114 -0
- package/dist/language-import.js +435 -0
- package/dist/lib/env-catalog.js +2 -2
- package/dist/lib/import-mdx.d.ts +38 -0
- package/dist/lib/import-mdx.js +411 -64
- package/dist/lib/render-comparison.d.ts +200 -0
- package/dist/lib/render-comparison.js +374 -0
- package/dist/lib/scaffold-render-sweep.d.ts +1 -1
- package/dist/lib/scaffold-render-sweep.js +1 -1
- package/dist/migrate-content.d.ts +6 -2
- package/dist/migrate-content.js +60 -14
- package/dist/module-architecture.js +6 -1
- package/dist/provision-env.d.ts +28 -0
- package/dist/provision-env.js +210 -0
- package/dist/seed-workspace.d.ts +3 -1
- package/dist/seed-workspace.js +45 -16
- package/dist/surfaces.js +3 -0
- package/dist/topology.d.ts +12 -0
- package/dist/topology.js +28 -10
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/content-source-guard.sh +75 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.env.example +7 -0
- package/templates/.github/workflows/ci.yml +2 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/apps/api/CLAUDE.md +6 -2
- package/templates/apps/mcp/.env.example +20 -0
- package/templates/apps/mcp/CLAUDE.md +46 -8
- package/templates/apps/web/.env.example +4 -2
- package/templates/apps/web/CLAUDE.md +39 -5
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +9 -1
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +58 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +16 -0
- package/templates/apps/web/app/.well-known/oauth-authorization-server/route.ts +53 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +8 -1
- package/templates/apps/web/app/[locale]/(auth)/consent/__tests__/consent-actions.test.ts +56 -0
- package/templates/apps/web/app/[locale]/(auth)/consent/__tests__/page.test.tsx +164 -0
- package/templates/apps/web/app/[locale]/(auth)/consent/consent-form.tsx +132 -0
- package/templates/apps/web/app/[locale]/(auth)/consent/consent.actions.ts +51 -0
- package/templates/apps/web/app/[locale]/(auth)/consent/page.tsx +144 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +153 -4
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
- package/templates/apps/web/app/[locale]/layout.tsx +122 -27
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/api/auth/hub/continuation.ts +8 -3
- package/templates/apps/web/app/api/auth/mcp/__tests__/continuation.test.ts +92 -0
- package/templates/apps/web/app/api/auth/mcp/continuation.ts +89 -0
- package/templates/apps/web/app/api/auth/mcp/continue/[ref]/__tests__/route.test.ts +59 -0
- package/templates/apps/web/app/api/auth/mcp/continue/[ref]/route.ts +41 -0
- package/templates/apps/web/app/llms.txt/route.ts +26 -9
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +40 -0
- package/templates/apps/web/app/openapi.json/route.ts +4 -0
- package/templates/apps/web/app/robots.txt/route.ts +18 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +13 -0
- package/templates/apps/web/auth.ts +184 -7
- package/templates/apps/web/jest.config.cjs +46 -1
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
- package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +90 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
- package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +11 -1
- package/templates/apps/web/lib/auth-routes.ts +5 -3
- package/templates/apps/web/lib/bot-check.ts +113 -13
- package/templates/apps/web/lib/in-page-magnet.ts +4 -1
- package/templates/apps/web/lib/mcp-oauth.ts +702 -0
- package/templates/apps/web/lib/page-chrome.ts +65 -5
- package/templates/apps/web/lib/relay.ts +22 -3
- package/templates/apps/web/lib/site-identity.ts +75 -24
- package/templates/apps/web/messages/en.json +47 -14
- package/templates/apps/web/next.config.mjs +15 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
- package/templates/apps/web/scripts/content-snapshot.ts +33 -0
- package/templates/database/seed.ts +6 -0
- package/templates/package.json +2 -1
- package/templates/tools/check-content-source.ts +108 -0
- package/versions.json +48 -48
|
@@ -25,9 +25,12 @@ import {
|
|
|
25
25
|
type SchemaOrgType,
|
|
26
26
|
} from "@working-theory/page-renderer";
|
|
27
27
|
import {
|
|
28
|
+
FooterStyleSchema,
|
|
28
29
|
HeaderStyleSchema,
|
|
29
30
|
ROUTE_FAMILY_KEYS,
|
|
30
31
|
SiteConfigSchema,
|
|
32
|
+
SiteFooterSchema,
|
|
33
|
+
type ClosingFooterAction,
|
|
31
34
|
type FooterStyle,
|
|
32
35
|
type HeaderStyle,
|
|
33
36
|
type RouteFamilyChrome,
|
|
@@ -130,11 +133,17 @@ export const readSiteChromeSettings = cache(
|
|
|
130
133
|
)
|
|
131
134
|
: undefined;
|
|
132
135
|
if (headerStyle !== undefined) settings.headerStyle = headerStyle;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
// #10550 D3 — the style alone, so an invalid closing-footer key
|
|
137
|
+
// (read by `readClosingFooterSettings`) never drops it.
|
|
138
|
+
const footer = record["footer"];
|
|
139
|
+
const footerStyle =
|
|
140
|
+
footer && typeof footer === "object"
|
|
141
|
+
? validated(
|
|
142
|
+
FooterStyleSchema,
|
|
143
|
+
(footer as Record<string, unknown>)["style"],
|
|
144
|
+
"footer.style",
|
|
145
|
+
)
|
|
146
|
+
: undefined;
|
|
138
147
|
if (footerStyle !== undefined) settings.footerStyle = footerStyle;
|
|
139
148
|
const layout = validated(
|
|
140
149
|
SiteConfigSchema.shape.layout,
|
|
@@ -157,6 +166,57 @@ export const readSiteChromeSettings = cache(
|
|
|
157
166
|
},
|
|
158
167
|
);
|
|
159
168
|
|
|
169
|
+
/** The closing footer's site settings (#10550 D3). */
|
|
170
|
+
export interface ClosingFooterSettings {
|
|
171
|
+
locality?: string;
|
|
172
|
+
actions?: ClosingFooterAction[];
|
|
173
|
+
legalBar: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `_site.mdx`'s closing-footer keys — `footer.locality`, `footer.actions`
|
|
178
|
+
* and `footer.legalBar` (#10550 D3). Each validates on its own, so one bad
|
|
179
|
+
* key drops only itself with one warning; an absent file or key is silent.
|
|
180
|
+
* A `footer` that is not an object, and each key `SiteFooterSchema` does not
|
|
181
|
+
* name, warn once too — the whole-object read this replaced warned on both.
|
|
182
|
+
*/
|
|
183
|
+
export const readClosingFooterSettings = cache(
|
|
184
|
+
function readClosingFooterSettings(): ClosingFooterSettings {
|
|
185
|
+
try {
|
|
186
|
+
const raw = readFileSync(join(resolveContentRoot(), "_site.mdx"), "utf8");
|
|
187
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
188
|
+
if (!match) return { legalBar: false };
|
|
189
|
+
const fm: unknown = parseYaml(match[1] ?? "");
|
|
190
|
+
if (!fm || typeof fm !== "object") return { legalBar: false };
|
|
191
|
+
const footer = (fm as Record<string, unknown>)["footer"];
|
|
192
|
+
if (footer === undefined) return { legalBar: false };
|
|
193
|
+
if (!footer || typeof footer !== "object" || Array.isArray(footer)) {
|
|
194
|
+
warnOnce("footer", "expected an object");
|
|
195
|
+
return { legalBar: false };
|
|
196
|
+
}
|
|
197
|
+
const record = footer as Record<string, unknown>;
|
|
198
|
+
const shape = SiteFooterSchema.shape;
|
|
199
|
+
for (const key of Object.keys(record)) {
|
|
200
|
+
if (!(key in shape)) warnOnce(`footer.${key}`, "unknown key");
|
|
201
|
+
}
|
|
202
|
+
const settings: ClosingFooterSettings = {
|
|
203
|
+
legalBar:
|
|
204
|
+
validated(shape.legalBar, record["legalBar"], "footer.legalBar") === true,
|
|
205
|
+
};
|
|
206
|
+
const locality = validated(shape.locality, record["locality"], "footer.locality");
|
|
207
|
+
if (locality !== undefined) settings.locality = locality;
|
|
208
|
+
const actions = validated(shape.actions, record["actions"], "footer.actions");
|
|
209
|
+
if (actions !== undefined) settings.actions = actions;
|
|
210
|
+
return settings;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if ((err as { code?: unknown }).code !== "ENOENT") {
|
|
213
|
+
warnOnce("frontmatter", "could not be read");
|
|
214
|
+
}
|
|
215
|
+
return { legalBar: false };
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
160
220
|
function camelCase(prefix: string): string {
|
|
161
221
|
return prefix
|
|
162
222
|
.replace(/^\//, "")
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { isIP } from "node:net";
|
|
51
51
|
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
getApiBase,
|
|
54
|
+
relayToApi,
|
|
55
|
+
visitorForwardHeaders,
|
|
56
|
+
} from "@working-theory/api-client";
|
|
53
57
|
|
|
54
58
|
import { browserApiOrigin } from "./api-origin.generated";
|
|
55
59
|
import { runBotCheck } from "./bot-check";
|
|
@@ -333,6 +337,21 @@ function hubConfigured(): boolean {
|
|
|
333
337
|
);
|
|
334
338
|
}
|
|
335
339
|
|
|
340
|
+
/**
|
|
341
|
+
* True when the API address `relayToApi` would call is this site's own host
|
|
342
|
+
* (#10553): the call would land back on this relay. (The API client itself
|
|
343
|
+
* refuses this server's own loopback port.)
|
|
344
|
+
*/
|
|
345
|
+
function apiIsOwnHost(request: Request): boolean {
|
|
346
|
+
try {
|
|
347
|
+
const api = new URL(getApiBase());
|
|
348
|
+
// Parsed under the API's scheme, so a default port compares equal.
|
|
349
|
+
return api.host === new URL(`${api.protocol}//${ownHost(request)}`).host;
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
336
355
|
/**
|
|
337
356
|
* The host the browser addressed: the first `x-forwarded-host`, else `host`.
|
|
338
357
|
* The site's edge must overwrite `x-forwarded-host`, never pass a client's on.
|
|
@@ -486,10 +505,10 @@ export async function handleRelay(request: Request): Promise<Response> {
|
|
|
486
505
|
body = text;
|
|
487
506
|
}
|
|
488
507
|
|
|
489
|
-
if (!hubConfigured()) {
|
|
508
|
+
if (!hubConfigured() || apiIsOwnHost(request)) {
|
|
490
509
|
console.error("[relay] hub call failed", {
|
|
491
510
|
path: entry.path,
|
|
492
|
-
outcome: "no-api-address",
|
|
511
|
+
outcome: hubConfigured() ? "own-address" : "no-api-address",
|
|
493
512
|
});
|
|
494
513
|
return refusal(502, "relay_unreachable", "The site could not reach its API");
|
|
495
514
|
}
|
|
@@ -21,6 +21,12 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
|
|
23
23
|
import {
|
|
24
|
+
resolvedAdapterKind,
|
|
25
|
+
type ContentAdapter,
|
|
26
|
+
} from "@working-theory/content-adapters";
|
|
27
|
+
import {
|
|
28
|
+
getContentAdapter,
|
|
29
|
+
setSiteFeatureListReader,
|
|
24
30
|
setSiteIdentity,
|
|
25
31
|
type SiteIdentityRegistration,
|
|
26
32
|
} from "@working-theory/page-renderer";
|
|
@@ -46,6 +52,59 @@ function readFrontmatter(path: string): Record<string, unknown> | undefined {
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A product page's feature titles. A `feature` section in the `steps` variant
|
|
57
|
+
* is how the product is set up, not what it does (the product page carries
|
|
58
|
+
* both, #6658 D3), so its step titles never enter the featureList.
|
|
59
|
+
*/
|
|
60
|
+
function featuresOf(page: { sections?: unknown }): string[] {
|
|
61
|
+
return featureListFromSections(
|
|
62
|
+
(Array.isArray(page.sections)
|
|
63
|
+
? (page.sections as Array<{ type: string; data?: unknown }>)
|
|
64
|
+
: []
|
|
65
|
+
).filter(
|
|
66
|
+
(section) =>
|
|
67
|
+
!(
|
|
68
|
+
section.type === "feature" &&
|
|
69
|
+
(section.data as { variant?: unknown } | undefined)?.variant === "steps"
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pages in the order the directory scan reads their seeds: by `<slug>.mdx`. */
|
|
76
|
+
function inSeedOrder<T extends { slug: string }>(pages: readonly T[]): T[] {
|
|
77
|
+
const key = (page: T): string => `${page.slug}.mdx`;
|
|
78
|
+
return [...pages].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The product's `featureList` read through the content adapter (#10644 D4):
|
|
83
|
+
* the first published `SoftwareProduct` page's features, else the titles of
|
|
84
|
+
* the published `Feature` pages, else none — the directory scan's first two
|
|
85
|
+
* steps, in the same seed order.
|
|
86
|
+
*/
|
|
87
|
+
export async function readFeatureListFromAdapter(
|
|
88
|
+
adapter: Pick<ContentAdapter, "listPages">,
|
|
89
|
+
locale: string,
|
|
90
|
+
): Promise<string[]> {
|
|
91
|
+
const [product] = inSeedOrder(await adapter.listPages("SoftwareProduct", locale));
|
|
92
|
+
if (product !== undefined) return featuresOf(product);
|
|
93
|
+
return inSeedOrder(await adapter.listPages("Feature", locale))
|
|
94
|
+
.map((page) => page.title)
|
|
95
|
+
.filter((title): title is string => typeof title === "string" && title.length > 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Whether the identity carries a product, whose featureList the graph shows. */
|
|
99
|
+
function carriesProduct(identity: SiteIdentityRegistration["identity"]): boolean {
|
|
100
|
+
return identity.kind === "Organization" && identity.secondary?.kind === "SoftwareApplication";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `_site.mdx`'s `defaultLocale`, else `"en"`. */
|
|
104
|
+
function localeOf(defaultLocale: unknown): string {
|
|
105
|
+
return typeof defaultLocale === "string" ? defaultLocale : "en";
|
|
106
|
+
}
|
|
107
|
+
|
|
49
108
|
/**
|
|
50
109
|
* The product's `featureList`: the feature titles of the product's own page —
|
|
51
110
|
* the first `SoftwareProduct` seed in the default locale, in sorted order
|
|
@@ -54,22 +113,6 @@ function readFrontmatter(path: string): Record<string, unknown> | undefined {
|
|
|
54
113
|
* first match in sorted order). Empty when there is neither.
|
|
55
114
|
*/
|
|
56
115
|
function readFeatureList(contentRoot: string, locale: string): string[] {
|
|
57
|
-
// A `feature` section in the `steps` variant is how the product is set up,
|
|
58
|
-
// not what it does (the product page carries both, #6658 D3), so its step
|
|
59
|
-
// titles never enter the featureList.
|
|
60
|
-
const featuresOf = (frontmatter: Record<string, unknown>): string[] =>
|
|
61
|
-
featureListFromSections(
|
|
62
|
-
(Array.isArray(frontmatter.sections)
|
|
63
|
-
? (frontmatter.sections as Array<{ type: string; data?: unknown }>)
|
|
64
|
-
: []
|
|
65
|
-
).filter(
|
|
66
|
-
(section) =>
|
|
67
|
-
!(
|
|
68
|
-
section.type === "feature" &&
|
|
69
|
-
(section.data as { variant?: unknown } | undefined)?.variant === "steps"
|
|
70
|
-
),
|
|
71
|
-
),
|
|
72
|
-
);
|
|
73
116
|
// #6658 D3: the product's own detail page (the first `SoftwareProduct`
|
|
74
117
|
// seed in sorted order) is where the feature sections live now.
|
|
75
118
|
const productDir = join(contentRoot, locale, "SoftwareProduct");
|
|
@@ -133,16 +176,24 @@ export function readSiteIdentity(
|
|
|
133
176
|
baseUrl: typeof baseUrl === "string" ? baseUrl : identity.url,
|
|
134
177
|
identity,
|
|
135
178
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
registration.featureList = readFeatureList(
|
|
141
|
-
contentRoot,
|
|
142
|
-
typeof defaultLocale === "string" ? defaultLocale : "en",
|
|
143
|
-
);
|
|
179
|
+
// Under the database adapter the list is read through the adapter when a
|
|
180
|
+
// page's JSON-LD is composed (#10644 D3), never scanned here.
|
|
181
|
+
if (carriesProduct(identity) && resolvedAdapterKind() !== "database") {
|
|
182
|
+
registration.featureList = readFeatureList(contentRoot, localeOf(defaultLocale));
|
|
144
183
|
}
|
|
145
184
|
return registration;
|
|
146
185
|
}
|
|
147
186
|
|
|
148
187
|
setSiteIdentity(readSiteIdentity());
|
|
188
|
+
|
|
189
|
+
// #10644 D3, D4 — under the database adapter the product's featureList is
|
|
190
|
+
// read through the adapter each time a page's JSON-LD is composed: an async
|
|
191
|
+
// read cannot run at module load.
|
|
192
|
+
if (resolvedAdapterKind() === "database") {
|
|
193
|
+
const contentRoot = resolveContentRoot();
|
|
194
|
+
const site = readSiteIdentity(contentRoot);
|
|
195
|
+
if (site !== undefined && carriesProduct(site.identity)) {
|
|
196
|
+
const locale = localeOf(readFrontmatter(join(contentRoot, "_site.mdx"))?.defaultLocale);
|
|
197
|
+
setSiteFeatureListReader(() => readFeatureListFromAdapter(getContentAdapter(), locale));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -179,7 +179,11 @@
|
|
|
179
179
|
},
|
|
180
180
|
"events": {
|
|
181
181
|
"heading": "Events",
|
|
182
|
-
"emptyState": "Events listing coming soon."
|
|
182
|
+
"emptyState": "Events listing coming soon.",
|
|
183
|
+
"addToCalendar": "Add to calendar",
|
|
184
|
+
"addToGoogleCalendar": "Google Calendar",
|
|
185
|
+
"addToOutlook": "Outlook",
|
|
186
|
+
"addToAppleCalendar": "Apple Calendar (.ics)"
|
|
183
187
|
},
|
|
184
188
|
"faq": {
|
|
185
189
|
"heading": "FAQ",
|
|
@@ -1399,10 +1403,11 @@
|
|
|
1399
1403
|
"toolChanges": "Show unsaved changes",
|
|
1400
1404
|
"backLabel": "Pages",
|
|
1401
1405
|
"statusDraft": "Draft",
|
|
1402
|
-
"statusNotYetLive": "Not yet live",
|
|
1403
1406
|
"statusPublished": "Published",
|
|
1407
|
+
"statusPublishedNewer": "Published, newer changes not live",
|
|
1404
1408
|
"publishedSwitch": "Published",
|
|
1405
|
-
"publishBlockedHint": "
|
|
1409
|
+
"publishBlockedHint": "Publish waits until the page is ready.",
|
|
1410
|
+
"publishChanges": "Publish changes",
|
|
1406
1411
|
"unsaved": "Unsaved",
|
|
1407
1412
|
"unsavedChanges": "Unsaved changes",
|
|
1408
1413
|
"discardButton": "Discard",
|
|
@@ -1427,15 +1432,16 @@
|
|
|
1427
1432
|
"toolLink": "Link",
|
|
1428
1433
|
"toolImage": "Insert image",
|
|
1429
1434
|
"toolView": "View",
|
|
1430
|
-
"toolRendered": "Rendered —
|
|
1435
|
+
"toolRendered": "Rendered — your words, formatted",
|
|
1431
1436
|
"toolCode": "Markdown — the words with their marks",
|
|
1437
|
+
"toolPreview": "Preview — the built page, with its sections",
|
|
1432
1438
|
"toolInsert": "Insert from your library",
|
|
1433
1439
|
"insertMeeting": "Add meeting",
|
|
1434
1440
|
"insertMagnet": "Add magnet",
|
|
1435
1441
|
"insertOffer": "Add offer",
|
|
1436
1442
|
"insertAsset": "Add asset",
|
|
1437
1443
|
"tips": {
|
|
1438
|
-
"h1": "Your
|
|
1444
|
+
"h1": "Your headline is the page's one H1; sections start at H2.",
|
|
1439
1445
|
"h2": "Types ## before the line → a section heading.",
|
|
1440
1446
|
"h3": "Types ### before the line → a smaller heading.",
|
|
1441
1447
|
"bold": "Types **word** → bold.",
|
|
@@ -1449,10 +1455,16 @@
|
|
|
1449
1455
|
"issueRequired": "{field} is required.",
|
|
1450
1456
|
"issueInvalid": "{field} isn't a valid choice.",
|
|
1451
1457
|
"forYou": {
|
|
1452
|
-
"
|
|
1453
|
-
|
|
1458
|
+
"tags": {
|
|
1459
|
+
"needsInput": "Needs input",
|
|
1460
|
+
"needsDecision": "Needs decision",
|
|
1461
|
+
"recommendation": "Recommendation"
|
|
1462
|
+
},
|
|
1454
1463
|
"fixWhy": "The page can't go live until this is fixed.",
|
|
1455
1464
|
"fixOutcome": "Once it is fixed, the Published switch turns on.",
|
|
1465
|
+
"checkWhy": "Publish waits until this is written.",
|
|
1466
|
+
"checkOutcome": "Once it is written, the page can go live.",
|
|
1467
|
+
"fix": "Fix",
|
|
1456
1468
|
"descriptionTitle": "Add a search description",
|
|
1457
1469
|
"descriptionWhy": "Search engines and AI answers show this line under the page's title. Without it they write their own.",
|
|
1458
1470
|
"descriptionOutcome": "A sentence you chose appears wherever this page is listed.",
|
|
@@ -1475,15 +1487,9 @@
|
|
|
1475
1487
|
"diffLegendAdded": "Added",
|
|
1476
1488
|
"diffLegendRemoved": "Removed",
|
|
1477
1489
|
"diffLegendUnchanged": "Unchanged",
|
|
1490
|
+
"bodyCodeOnly": "This page has markup only the Code view can show, so it opens in Code.",
|
|
1478
1491
|
"tagPlaceholder": "{kind}: {slug}"
|
|
1479
1492
|
},
|
|
1480
|
-
"sections": {
|
|
1481
|
-
"heading": "Sections",
|
|
1482
|
-
"arrayAdd": "Add",
|
|
1483
|
-
"arrayRemove": "Remove",
|
|
1484
|
-
"unsupportedField": "This field is edited as raw JSON",
|
|
1485
|
-
"sectionLevelErrors": "Other problems with this section"
|
|
1486
|
-
},
|
|
1487
1493
|
"body": {
|
|
1488
1494
|
"label": "Body",
|
|
1489
1495
|
"hint": "Markdown. Headings, links, lists and images all work.",
|
|
@@ -1531,5 +1537,32 @@
|
|
|
1531
1537
|
"body": "The link may have expired. Go back to the site you came from and sign in again.",
|
|
1532
1538
|
"back": "Back to your account"
|
|
1533
1539
|
}
|
|
1540
|
+
},
|
|
1541
|
+
"mcpConsent": {
|
|
1542
|
+
"metaTitle": "Connect an app",
|
|
1543
|
+
"heading": "Connect an app to a workspace",
|
|
1544
|
+
"signedInAs": "Signed in as {email}.",
|
|
1545
|
+
"signOut": "Not you? Sign out",
|
|
1546
|
+
"clientClaim": "{name} — this app registered itself; the name is its own claim and has not been checked.",
|
|
1547
|
+
"returnsTo": "If you allow it, access is sent to {origin}.",
|
|
1548
|
+
"workspaceLegend": "Choose the workspace this app may use",
|
|
1549
|
+
"scopesLegend": "What it may also do there",
|
|
1550
|
+
"memberReads": "Any approval also lets the app read what a member can read in that workspace.",
|
|
1551
|
+
"noScopes": "Your role in this workspace offers nothing beyond member reads.",
|
|
1552
|
+
"allow": "Allow",
|
|
1553
|
+
"deny": "Deny",
|
|
1554
|
+
"unavailable": "This request can't continue. Go back to the app and connect again.",
|
|
1555
|
+
"noWorkspace": "You have no workspace to connect this app to.",
|
|
1556
|
+
"scopes": {
|
|
1557
|
+
"content-edit": "Edit pages and docs",
|
|
1558
|
+
"content-publish": "Publish pages and docs",
|
|
1559
|
+
"assets-write": "Upload and replace files",
|
|
1560
|
+
"magnets-manage": "Manage forms and magnets",
|
|
1561
|
+
"meeting-types-manage": "Manage meeting types",
|
|
1562
|
+
"people-read": "See your people",
|
|
1563
|
+
"member-pieces-manage": "Manage what members see",
|
|
1564
|
+
"people-memory-propose": "Suggest notes about people",
|
|
1565
|
+
"inbox-act": "Act on the inbox"
|
|
1566
|
+
}
|
|
1534
1567
|
}
|
|
1535
1568
|
}
|
|
@@ -6,6 +6,7 @@ import { withSentryConfig } from "@sentry/nextjs";
|
|
|
6
6
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
7
7
|
import { DocsPathSchema, ServicesPathSchema } from "@working-theory/site-config/schema";
|
|
8
8
|
import { legacyRedirects, ROUTES, SERVICES_ROUTE } from "@working-theory/templates/routes";
|
|
9
|
+
import { assetRemotePattern } from "@working-theory/validation";
|
|
9
10
|
import { parseStealth } from "@working-theory/web-i18n/stealth";
|
|
10
11
|
import { withBotId } from "botid/next/config";
|
|
11
12
|
|
|
@@ -279,6 +280,14 @@ function readServiceParents() {
|
|
|
279
280
|
const servicesPathAtBuild = readServicesPathFromSiteMdx();
|
|
280
281
|
readServiceParents();
|
|
281
282
|
|
|
283
|
+
// #10647 D6 — a database spoke's pages name library images as `asset:<key>`,
|
|
284
|
+
// which the hub resolves to its own `/v1/assets/:id` URLs. `next/image` admits
|
|
285
|
+
// that path on the hub's origin, and nothing else, when `AILK_API_URL` (the
|
|
286
|
+
// server-side hub origin this site already reads) is an http(s) URL at build
|
|
287
|
+
// time; otherwise there is no `images` key, as before. A change of hub origin
|
|
288
|
+
// needs a rebuild.
|
|
289
|
+
const assetPatternAtBuild = assetRemotePattern(process.env.AILK_API_URL);
|
|
290
|
+
|
|
282
291
|
/** @type {import('next').NextConfig} */
|
|
283
292
|
const nextConfig = {
|
|
284
293
|
reactStrictMode: true,
|
|
@@ -302,7 +311,9 @@ const nextConfig = {
|
|
|
302
311
|
// content/ directory that carries the schema-typed pages, so the standalone
|
|
303
312
|
// output shipped without them and postbuild's assert-content-bundled failed.
|
|
304
313
|
outputFileTracingIncludes: {
|
|
305
|
-
|
|
314
|
+
// #10605 D6 — the last-good content snapshot. An exact path, never a glob
|
|
315
|
+
// over `.ailk/`: that directory also holds the lead-inbox fallback.
|
|
316
|
+
"/**": ["../../content/**/*.mdx", "./.ailk/content-snapshot.json"],
|
|
306
317
|
// #10041 D8 — /sitemap.xml and /llms.txt list the docs by reading this
|
|
307
318
|
// app's own content/<locale>/docs/ at request time.
|
|
308
319
|
"/sitemap.xml": ["./content/**/*.mdx"],
|
|
@@ -349,6 +360,9 @@ const nextConfig = {
|
|
|
349
360
|
// mutates a tracked file as a side effect of running the dev server,
|
|
350
361
|
// which this scaffold does not want.
|
|
351
362
|
agentRules: false,
|
|
363
|
+
...(assetPatternAtBuild === null
|
|
364
|
+
? {}
|
|
365
|
+
: { images: { remotePatterns: [assetPatternAtBuild] } }),
|
|
352
366
|
env: {
|
|
353
367
|
// Comma-separated locale list baked at build time — consumed by middleware
|
|
354
368
|
// and the sitemap route. NEXT_PUBLIC_ prefix makes it available in Edge
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "next dev --webpack",
|
|
9
|
-
"build": "tsx scripts/check-nav-labels.ts && next build --webpack",
|
|
9
|
+
"build": "tsx scripts/check-nav-labels.ts && tsx scripts/content-snapshot.ts && next build --webpack",
|
|
10
10
|
"postbuild": "tsx scripts/assert-content-bundled.ts",
|
|
11
11
|
"start": "next start",
|
|
12
12
|
"type-check": "tsc --noEmit",
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* preserving their path relative to outputFileTracingRoot (repo root). So
|
|
6
6
|
* <repo-root>/content/en/HomePage/home.mdx → .next/standalone/content/en/...
|
|
7
7
|
*
|
|
8
|
+
* Under CONTENT_ADAPTER=database (#10646 D1) a site's pages live on its hub,
|
|
9
|
+
* so content/ may hold no page at all. The build's own content file is then
|
|
10
|
+
* the snapshot (#10605), traced by exact path: when the build wrote one, the
|
|
11
|
+
* sentinel is apps/web/.ailk/content-snapshot.json →
|
|
12
|
+
* .next/standalone/apps/web/.ailk/content-snapshot.json. Without one (an
|
|
13
|
+
* unreachable hub, #10605 D3), the .mdx walk below still runs.
|
|
14
|
+
*
|
|
8
15
|
* See: docs/runbooks/page-renderer-deploy-verification.md §1 Cause 2
|
|
9
16
|
*/
|
|
10
17
|
|
|
@@ -38,12 +45,25 @@ function firstMdx(dir: string): string | undefined {
|
|
|
38
45
|
}
|
|
39
46
|
return undefined;
|
|
40
47
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
const repoRoot = resolve(appWebDir, "..", "..");
|
|
49
|
+
const snapshotAbs = join(appWebDir, ".ailk", "content-snapshot.json");
|
|
50
|
+
const database = process.env.CONTENT_ADAPTER === "database";
|
|
51
|
+
const snapshotSentinel = database && existsSync(snapshotAbs);
|
|
52
|
+
let sentinelRelative: string;
|
|
53
|
+
if (snapshotSentinel) {
|
|
54
|
+
sentinelRelative = relative(repoRoot, snapshotAbs);
|
|
55
|
+
} else {
|
|
56
|
+
const sentinelAbs = firstMdx(contentDir);
|
|
57
|
+
if (sentinelAbs === undefined) {
|
|
58
|
+
console.error(
|
|
59
|
+
database
|
|
60
|
+
? "[assert-content-bundled] ERROR: no content snapshot (.ailk/content-snapshot.json) and no .mdx file under content/ to trace."
|
|
61
|
+
: "[assert-content-bundled] ERROR: no .mdx file under content/ to trace.",
|
|
62
|
+
);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
sentinelRelative = join("content", relative(contentDir, sentinelAbs));
|
|
45
66
|
}
|
|
46
|
-
const sentinelRelative = join("content", relative(contentDir, sentinelAbs));
|
|
47
67
|
const sentinelPath = join(standaloneDir, sentinelRelative);
|
|
48
68
|
|
|
49
69
|
if (!existsSync(standaloneDir)) {
|
|
@@ -60,11 +80,19 @@ if (!existsSync(sentinelPath)) {
|
|
|
60
80
|
);
|
|
61
81
|
console.error(" Expected path:", sentinelPath);
|
|
62
82
|
console.error(
|
|
63
|
-
|
|
83
|
+
snapshotSentinel
|
|
84
|
+
? " This means outputFileTracingIncludes did not bundle the content snapshot into the build artifact."
|
|
85
|
+
: " This means outputFileTracingIncludes did not bundle content/ into the build artifact.",
|
|
86
|
+
);
|
|
87
|
+
console.error(
|
|
88
|
+
snapshotSentinel
|
|
89
|
+
? " The deployed site cannot fall back to its last-good hub content."
|
|
90
|
+
: " The deployed site cannot serve MDX content.",
|
|
64
91
|
);
|
|
65
|
-
console.error(" The deployed site cannot serve MDX content.");
|
|
66
92
|
console.error(
|
|
67
|
-
|
|
93
|
+
` Fix: verify outputFileTracingIncludes in apps/web/next.config.mjs covers ${
|
|
94
|
+
snapshotSentinel ? "'./.ailk/content-snapshot.json'" : "'../../content/**/*.mdx'"
|
|
95
|
+
}.`,
|
|
68
96
|
);
|
|
69
97
|
console.error(
|
|
70
98
|
" See: docs/runbooks/page-renderer-deploy-verification.md §1 Cause 2",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write the last-good content snapshot (#10605 D5). Runs in `pnpm build`
|
|
3
|
+
* before `next build` — never in `prebuild`, which the docs-blog module owns
|
|
4
|
+
* in a scaffold.
|
|
5
|
+
*
|
|
6
|
+
* Under `CONTENT_ADAPTER=database` it reads every published page from the hub
|
|
7
|
+
* into `.ailk/content-snapshot.json`; under any other adapter it does nothing.
|
|
8
|
+
* An unreachable hub keeps the previous snapshot and the build goes on; only
|
|
9
|
+
* a configuration fault (a 4xx other than 404/429) fails the build.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { refreshContentSnapshot } from "@working-theory/page-renderer/snapshot";
|
|
15
|
+
import { readLocales } from "@working-theory/web-i18n";
|
|
16
|
+
|
|
17
|
+
function enabledLocales(): string[] {
|
|
18
|
+
try {
|
|
19
|
+
return readLocales(join(process.cwd(), "..", ".."));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// next.config.mjs's own fallback when project.yaml has no locales.
|
|
22
|
+
console.warn("[content-snapshot] locales unreadable, using en:", error);
|
|
23
|
+
return ["en"];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
refreshContentSnapshot({ locales: enabledLocales() }).then(
|
|
28
|
+
(outcome) => console.log(`[content-snapshot] ${outcome}`),
|
|
29
|
+
(error: unknown) => {
|
|
30
|
+
console.error("[content-snapshot] ERROR:", error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
REQUEST_QUOTE_FLOW_SLUG,
|
|
42
42
|
REQUEST_QUOTE_FLOW_TITLE,
|
|
43
43
|
requestQuoteFlowConfig,
|
|
44
|
+
STANDARD_MAGNET_TAGS,
|
|
44
45
|
TRIAL_FLOW_LANDING_PAGE,
|
|
45
46
|
TRIAL_FLOW_SLUG,
|
|
46
47
|
TRIAL_FLOW_TITLE,
|
|
@@ -150,6 +151,7 @@ export async function provisionDefaultWorkspace(
|
|
|
150
151
|
status: "draft",
|
|
151
152
|
customersEnabled: true,
|
|
152
153
|
completion: "invite",
|
|
154
|
+
tags: [STANDARD_MAGNET_TAGS[REQUEST_QUOTE_FLOW_SLUG]!],
|
|
153
155
|
},
|
|
154
156
|
update: {},
|
|
155
157
|
});
|
|
@@ -171,6 +173,7 @@ export async function provisionDefaultWorkspace(
|
|
|
171
173
|
status: "live",
|
|
172
174
|
customersEnabled: false,
|
|
173
175
|
completion: "lead",
|
|
176
|
+
tags: [STANDARD_MAGNET_TAGS[CONTACT_FORM_FLOW_SLUG]!],
|
|
174
177
|
landingPage: CONTACT_FORM_LANDING_PAGE,
|
|
175
178
|
},
|
|
176
179
|
update: {},
|
|
@@ -193,6 +196,7 @@ export async function provisionDefaultWorkspace(
|
|
|
193
196
|
status: "live",
|
|
194
197
|
customersEnabled: false,
|
|
195
198
|
completion: "invite",
|
|
199
|
+
tags: [STANDARD_MAGNET_TAGS[JOB_APPLICATION_FLOW_SLUG]!],
|
|
196
200
|
landingPage: JOB_APPLICATION_LANDING_PAGE,
|
|
197
201
|
},
|
|
198
202
|
update: {},
|
|
@@ -216,6 +220,7 @@ export async function provisionDefaultWorkspace(
|
|
|
216
220
|
status: "live",
|
|
217
221
|
customersEnabled: false,
|
|
218
222
|
completion: "lead",
|
|
223
|
+
tags: [STANDARD_MAGNET_TAGS[NEWSLETTER_FLOW_SLUG]!],
|
|
219
224
|
landingPage: NEWSLETTER_LANDING_PAGE,
|
|
220
225
|
},
|
|
221
226
|
update: {},
|
|
@@ -239,6 +244,7 @@ export async function provisionDefaultWorkspace(
|
|
|
239
244
|
status: "live",
|
|
240
245
|
customersEnabled: false,
|
|
241
246
|
completion: "lead",
|
|
247
|
+
tags: [STANDARD_MAGNET_TAGS[TRIAL_FLOW_SLUG]!],
|
|
242
248
|
landingPage: TRIAL_FLOW_LANDING_PAGE,
|
|
243
249
|
},
|
|
244
250
|
update: {},
|
package/templates/package.json
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"db:studio": "prisma studio --config database/prisma.config.ts",
|
|
21
21
|
"architecture": "tsx tools/lint-architecture.ts",
|
|
22
22
|
"claude-md:check": "tsx tools/check-claude-md.ts",
|
|
23
|
-
"
|
|
23
|
+
"content-source:check": "tsx tools/check-content-source.ts",
|
|
24
|
+
"preflight": "pnpm architecture && pnpm claude-md:check && pnpm content-source:check && pnpm type-check && pnpm lint && pnpm build"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@arethetypeswrong/cli": "^0.18.5",
|