create-ailk 0.6.1 → 0.7.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 +43 -4
- package/component-catalog.json +146 -21
- package/component-catalog.md +9 -6
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +73 -11
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +56 -1
- package/dist/lib/extract-module-bundle.js +165 -13
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +26 -3
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +7 -0
- package/dist/parse-args.js +23 -1
- package/dist/programmatic.d.ts +46 -0
- package/dist/programmatic.js +130 -9
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +494 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +149 -28
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +58 -2
- package/dist/template-manifest.js +196 -69
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +162 -0
- package/site-templates/marketing-site/DESIGN.md +55 -41
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +55 -41
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/minimalism-site/DESIGN.md +54 -40
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/site-templates/saas-product/DESIGN.md +55 -41
- package/site-templates/simple-service/DESIGN.md +55 -41
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
- package/site-templates/simple-service/ailk-template.json +4 -3
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
- package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +8 -8
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +111 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +8 -2
- package/templates/DESIGN.md +52 -45
- package/templates/README.md +108 -46
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
- package/templates/apps/api/src/config/index.ts +19 -6
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/server.ts +6 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
- package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +113 -51
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +64 -12
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +1 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +134 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +52 -37
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/content/_site.mdx +23 -0
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +105 -0
- package/templates/content/en/ContactPage/contact.mdx +44 -85
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +276 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
- package/templates/content/en/SoftwareProduct/product.mdx +175 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -1,48 +1,24 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax -- i18n-allow: not-found boundary renders outside the [locale] intl provider; translations unavailable at this segment (ADR 0019) */
|
|
2
2
|
// Next.js App Router not-found boundary. Renders for any route that calls
|
|
3
3
|
// notFound() or when no route matches. Server Component by default.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
//
|
|
5
|
+
// This boundary renders under the pass-through root layout, outside
|
|
6
|
+
// app/[locale]/layout.tsx — the only other importer of globals.css — so it
|
|
7
|
+
// imports the stylesheet itself; without it the primitives' token classes
|
|
8
|
+
// would render unstyled here (#6514).
|
|
9
|
+
import "./globals.css";
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
background: tokens.colors.light.bg,
|
|
10
|
-
color: tokens.colors.light.fg,
|
|
11
|
-
minHeight: "100vh",
|
|
12
|
-
display: "flex",
|
|
13
|
-
flexDirection: "column" as const,
|
|
14
|
-
alignItems: "center",
|
|
15
|
-
justifyContent: "center",
|
|
16
|
-
padding: tokens.radius.lg,
|
|
17
|
-
gap: tokens.radius.md,
|
|
18
|
-
},
|
|
19
|
-
heading: {
|
|
20
|
-
fontSize: "1.5rem",
|
|
21
|
-
lineHeight: tokens.lineHeight.md,
|
|
22
|
-
margin: 0,
|
|
23
|
-
},
|
|
24
|
-
body: {
|
|
25
|
-
fontSize: "1rem",
|
|
26
|
-
lineHeight: tokens.lineHeight.md,
|
|
27
|
-
margin: 0,
|
|
28
|
-
},
|
|
29
|
-
link: {
|
|
30
|
-
color: tokens.colors.light.brand,
|
|
31
|
-
fontSize: "1rem",
|
|
32
|
-
lineHeight: tokens.lineHeight.md,
|
|
33
|
-
},
|
|
34
|
-
} as const;
|
|
11
|
+
import { Heading, Link, Text } from "@working-theory/ui/primitives";
|
|
12
|
+
import type { ReactElement } from "react";
|
|
35
13
|
|
|
36
14
|
export default function NotFound(): ReactElement {
|
|
37
15
|
return (
|
|
38
|
-
<main
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
Return home
|
|
45
|
-
</a>
|
|
16
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-4 bg-(--color-bg) p-card text-(--color-fg)">
|
|
17
|
+
<Heading level={1} visual={3}>
|
|
18
|
+
We couldn't find that page
|
|
19
|
+
</Heading>
|
|
20
|
+
<Text>The page you are looking for does not exist or may have moved.</Text>
|
|
21
|
+
<Link href="/">Return home</Link>
|
|
46
22
|
</main>
|
|
47
23
|
);
|
|
48
24
|
}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* - Strip control characters \x00–\x1F and \x7F from title and schemaType
|
|
16
16
|
* - Cap title at 200 chars, schemaType at 50 chars
|
|
17
17
|
* - If sanitization yields an empty title, fall back to static PNG redirect
|
|
18
|
+
*
|
|
19
|
+
* Composition exception (#6514): this route renders raw elements with inline
|
|
20
|
+
* `style` objects, and is deliberately left out of the page-surface
|
|
21
|
+
* `no-raw-html-elements` scope in eslint.config.ts. Satori (next/og) rasterises
|
|
22
|
+
* a JSX tree to PNG — it reads only inline styles, supports no stylesheet or
|
|
23
|
+
* Tailwind classes, and cannot render @working-theory/ui components.
|
|
18
24
|
*/
|
|
19
25
|
import { clientError } from "@working-theory/page-renderer/edge";
|
|
20
26
|
import { tokens } from "@working-theory/theme";
|
|
@@ -28,6 +28,9 @@ export default tseslint.config(
|
|
|
28
28
|
"**/dist/**",
|
|
29
29
|
"**/generated/**",
|
|
30
30
|
"**/node_modules/**",
|
|
31
|
+
// fumadocs-mdx's generated collection code (#6680) — present only with
|
|
32
|
+
// the docs-blog module, and never hand-edited.
|
|
33
|
+
"**/.source/**",
|
|
31
34
|
],
|
|
32
35
|
},
|
|
33
36
|
js.configs.recommended,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js instrumentation hook — runs once per server instance at boot.
|
|
3
|
+
*
|
|
4
|
+
* Registers the durable home of a renewed licence key (#6677 DD-6) before any
|
|
5
|
+
* request resolves the licence. Node runtime only: the store reaches the
|
|
6
|
+
* database through Prisma, which the Edge runtime cannot load.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export async function register(): Promise<void> {
|
|
10
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
11
|
+
const { registerLicenseKeyStore } = await import("./lib/license-key-store");
|
|
12
|
+
await registerLicenseKeyStore();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -56,6 +56,10 @@ import {
|
|
|
56
56
|
import type { ReactElement } from "react";
|
|
57
57
|
import { parse as parseYaml } from "yaml";
|
|
58
58
|
|
|
59
|
+
// Registers the site's `_site.mdx` identity at module load, as the `[locale]`
|
|
60
|
+
// layout does in production (#6597).
|
|
61
|
+
import { readSiteIdentity } from "../site-identity";
|
|
62
|
+
|
|
59
63
|
// Detail routes call notFound() on a missing page; a thrown error is how the
|
|
60
64
|
// test sees it (the same shape the per-surface suites use).
|
|
61
65
|
jest.mock("next/navigation", () => ({
|
|
@@ -187,6 +191,38 @@ function extractJsonLdBlocks(container: HTMLElement): JsonLdObject[] {
|
|
|
187
191
|
return blocks;
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
const SITE_IDENTITY = readSiteIdentity(CONTENT_ROOT);
|
|
195
|
+
const SITE_ORIGIN = (
|
|
196
|
+
process.env.NEXT_PUBLIC_SITE_URL ??
|
|
197
|
+
process.env.MARKETING_WEB_BASE_URL ??
|
|
198
|
+
"http://localhost:3000"
|
|
199
|
+
).replace(/\/+$/, "");
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The sitewide identity trio every rendered page carries when the site's own
|
|
203
|
+
* `_site.mdx` reads (#6597): exactly one identity node at its `@id`, exactly
|
|
204
|
+
* one `WebSite` published by it, and the page's primary linked `isPartOf`.
|
|
205
|
+
* `null` when the page carries all three.
|
|
206
|
+
*/
|
|
207
|
+
function identityFailure(blocks: JsonLdObject[], label: string): string | null {
|
|
208
|
+
if (SITE_IDENTITY === undefined) return null;
|
|
209
|
+
const identityId = `${SITE_ORIGIN}/#${SITE_IDENTITY.identity.kind === "Person" ? "person" : "organization"}`;
|
|
210
|
+
const webSiteId = `${SITE_ORIGIN}/#website`;
|
|
211
|
+
const identity = blocks.filter((b) => b["@id"] === identityId);
|
|
212
|
+
const webSite = blocks.filter((b) => b["@id"] === webSiteId);
|
|
213
|
+
const problems: string[] = [];
|
|
214
|
+
if (identity.length !== 1 || identity[0]?.["@type"] !== SITE_IDENTITY.identity.kind)
|
|
215
|
+
problems.push(`expected one ${SITE_IDENTITY.identity.kind} at ${identityId}, got ${identity.length}`);
|
|
216
|
+
if (webSite.length !== 1 || webSite[0]?.["@type"] !== "WebSite")
|
|
217
|
+
problems.push(`expected one WebSite at ${webSiteId}, got ${webSite.length}`);
|
|
218
|
+
else if ((webSite[0]["publisher"] as { "@id"?: unknown } | undefined)?.["@id"] !== identityId)
|
|
219
|
+
problems.push(`WebSite publisher is not {"@id": "${identityId}"}`);
|
|
220
|
+
// A page's own `isPartOf` wins over the sitewide link (composeGraph), so the
|
|
221
|
+
// check is that the primary is linked, not what to.
|
|
222
|
+
if (blocks[0]?.["isPartOf"] === undefined) problems.push("the primary carries no isPartOf");
|
|
223
|
+
return problems.length === 0 ? null : `${label}: sitewide identity — ${problems.join("; ")}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
190
226
|
// ── 1. Content-driven: every content page builds valid JSON-LD ──────────────
|
|
191
227
|
|
|
192
228
|
describe("structured data — every content page builds valid JSON-LD", () => {
|
|
@@ -510,6 +546,8 @@ describe("structured data — every route emits a valid JSON-LD script", () => {
|
|
|
510
546
|
}
|
|
511
547
|
const failure = validateFor(blocks, type, pageLabel);
|
|
512
548
|
if (failure !== null) failures.push(failure);
|
|
549
|
+
const missingIdentity = identityFailure(blocks, pageLabel);
|
|
550
|
+
if (missingIdentity !== null) failures.push(missingIdentity);
|
|
513
551
|
}
|
|
514
552
|
expect(failures).toEqual([]);
|
|
515
553
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the durable home of a renewed licence key (#6677 DD-6).
|
|
3
|
+
*
|
|
4
|
+
* `@working-theory/license` renews the deployment's key before it lapses and
|
|
5
|
+
* keeps the renewed key in a `LicenseKeyStore`. Its default store is a file,
|
|
6
|
+
* which a redeploy wipes — and this app's one real `checkLicense()` caller
|
|
7
|
+
* (`src/lib/commerce-claim.ts`, gating /purchase/* and /.well-known/mcp.json)
|
|
8
|
+
* would then run on the older `AILK_LICENSE_KEY` alone. With a database
|
|
9
|
+
* configured, this registers the store backed by ONE deployment-wide
|
|
10
|
+
* `SiteSetting` row, `(DEFAULT_WORKSPACE_ID, "license.renewed_key")`, shared
|
|
11
|
+
* with apps/api and apps/mcp.
|
|
12
|
+
*
|
|
13
|
+
* NARROW, NAMED EXCEPTION to "apps/web never touches the database directly"
|
|
14
|
+
* (spec 6677 DD-6 and § 7; architecture.yaml `web.description`; the
|
|
15
|
+
* `prisma-for-auth.ts` header): this file, and only this file, hands
|
|
16
|
+
* `prismaForAuth.siteSetting` to `createSiteSettingLicenseKeyStore`, which
|
|
17
|
+
* reads and writes the one `license.renewed_key` row and nothing else. The
|
|
18
|
+
* alternative — an apps/api route serving the key over HTTP — is a hard floor
|
|
19
|
+
* and a wider exposure of a bearer credential.
|
|
20
|
+
*
|
|
21
|
+
* Imported from `instrumentation.ts` `register()` on the Node runtime only.
|
|
22
|
+
* Both imports are dynamic and reached only when `DATABASE_URL` is set.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register the SiteSetting-backed store when `DATABASE_URL` is set. An
|
|
27
|
+
* explicitly set `AILK_LICENSE_STORAGE_PATH` is the operator choosing the
|
|
28
|
+
* file store, and is honoured. Never throws: a failure logs and leaves the
|
|
29
|
+
* file store in place.
|
|
30
|
+
*/
|
|
31
|
+
export async function registerLicenseKeyStore(): Promise<void> {
|
|
32
|
+
if (!process.env.DATABASE_URL || process.env.AILK_LICENSE_STORAGE_PATH) return;
|
|
33
|
+
try {
|
|
34
|
+
const [{ prismaForAuth: prisma }, { DEFAULT_WORKSPACE_ID }, license] =
|
|
35
|
+
await Promise.all([
|
|
36
|
+
import("./prisma-for-auth"),
|
|
37
|
+
import("@working-theory/database/tenancy"),
|
|
38
|
+
import("@working-theory/license"),
|
|
39
|
+
]);
|
|
40
|
+
license.setLicenseKeyStore(
|
|
41
|
+
license.createSiteSettingLicenseKeyStore(
|
|
42
|
+
prisma.siteSetting,
|
|
43
|
+
DEFAULT_WORKSPACE_ID,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(
|
|
48
|
+
"[web] could not register the SiteSetting licence-key store — the file store stays in place:",
|
|
49
|
+
err instanceof Error ? err.name : "non-Error thrown",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,33 +1,85 @@
|
|
|
1
|
-
// nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469
|
|
1
|
+
// nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469,
|
|
2
|
+
// #6508).
|
|
2
3
|
//
|
|
3
|
-
// The Free marketing-site template's nav (the approved chrome, #5469 D5
|
|
4
|
-
// a Product flyout (How it works · Features · FAQ)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// The Free marketing-site template's nav (the approved chrome, #5469 D5,
|
|
5
|
+
// #6508 D5): a Product flyout (How it works · Features · FAQ) rendered as a
|
|
6
|
+
// fixed-640px two-column panel (the Standard Issue direction — #6508), then
|
|
7
|
+
// How it works, Pricing and About as top-level items. Paths are
|
|
8
|
+
// locale-agnostic; Nav prefixes them with the active locale at render.
|
|
9
|
+
// `footerColumns` carries the footer's column groups (#6468), keys only — the
|
|
10
|
+
// layout resolves the labels.
|
|
7
11
|
//
|
|
8
12
|
// In a SCAFFOLD this file is regenerated by create-ailk from the declared
|
|
9
13
|
// route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
|
|
10
14
|
// so the nav never links a page the scaffold does not carry — keep the two in
|
|
11
15
|
// step when the base item set changes.
|
|
16
|
+
//
|
|
17
|
+
// SOFT_LOCK_OK: #6508 — Product's flyout is `width: "panel"` (D4/D5): a fixed
|
|
18
|
+
// 640px, two-column grid, replacing the prior "list"/"trigger" one-column
|
|
19
|
+
// menu. `presentation` is omitted — it defaults to "grid" (D5). The `docs`
|
|
20
|
+
// action is dropped (it duplicated the first feature's /how-it-works
|
|
21
|
+
// destination, § 1.4). This note lives here, not inside the `navItems` array
|
|
22
|
+
// below, so this file's `navItems`/`footerColumns` blocks stay byte-identical
|
|
23
|
+
// to `renderNavConfig`'s generated output — the mirror test (#6468) diffs the
|
|
24
|
+
// arrays verbatim.
|
|
12
25
|
|
|
13
26
|
import type { NavItem } from "@working-theory/ui";
|
|
14
|
-
import {
|
|
27
|
+
import { HelpCircle, Layers, Sparkles } from "lucide-react";
|
|
15
28
|
|
|
16
29
|
export const navItems: NavItem[] = [
|
|
17
30
|
{
|
|
18
31
|
labelKey: "product",
|
|
19
32
|
flyout: {
|
|
20
|
-
|
|
21
|
-
width: "trigger",
|
|
33
|
+
width: "panel",
|
|
22
34
|
features: [
|
|
23
|
-
{ labelKey: "
|
|
24
|
-
{ labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/
|
|
35
|
+
{ labelKey: "useCases", descKey: "useCasesDesc", icon: Layers, path: "/use-cases" },
|
|
36
|
+
{ labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/software/product" },
|
|
25
37
|
{ labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
|
|
26
38
|
],
|
|
27
|
-
actions: [
|
|
39
|
+
actions: [],
|
|
28
40
|
},
|
|
29
41
|
},
|
|
30
|
-
{ labelKey: "
|
|
42
|
+
{ labelKey: "useCases", path: "/use-cases" },
|
|
43
|
+
{ labelKey: "blog", path: "/blog" },
|
|
31
44
|
{ labelKey: "pricing", path: "/pricing" },
|
|
32
45
|
{ labelKey: "about", path: "/about" },
|
|
33
46
|
];
|
|
47
|
+
|
|
48
|
+
export interface FooterColumnConfig {
|
|
49
|
+
headingKey: string;
|
|
50
|
+
items: { labelKey: string; path: string }[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const footerColumns: FooterColumnConfig[] = [
|
|
54
|
+
{
|
|
55
|
+
headingKey: "footerSitePagesHeading",
|
|
56
|
+
items: [
|
|
57
|
+
{ labelKey: "features", path: "/software/product" },
|
|
58
|
+
{ labelKey: "pricing", path: "/pricing" },
|
|
59
|
+
{ labelKey: "useCases", path: "/use-cases" },
|
|
60
|
+
{ labelKey: "about", path: "/about" },
|
|
61
|
+
{ labelKey: "contact", path: "/contact" },
|
|
62
|
+
{ labelKey: "bookACall", path: "/book-a-call" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
headingKey: "footerFeaturesHeading",
|
|
67
|
+
items: [
|
|
68
|
+
{ labelKey: "feature1", path: "/software/product" },
|
|
69
|
+
{ labelKey: "feature2", path: "/software/product" },
|
|
70
|
+
{ labelKey: "feature3", path: "/software/product" },
|
|
71
|
+
{ labelKey: "feature4", path: "/software/product" },
|
|
72
|
+
{ labelKey: "feature5", path: "/software/product" },
|
|
73
|
+
{ labelKey: "allFeatures", path: "/software/product" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
headingKey: "footerResourcesHeading",
|
|
78
|
+
items: [
|
|
79
|
+
{ labelKey: "blog", path: "/blog" },
|
|
80
|
+
{ labelKey: "glossary", path: "/glossary" },
|
|
81
|
+
{ labelKey: "newsletter", path: "/newsletter" },
|
|
82
|
+
{ labelKey: "faq", path: "/faq" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
* Better Auth migration (#587). Driver adapter wrapping landed in Phase B
|
|
6
6
|
* of the Prisma 7 migration (#661) ahead of the v7 bump in Phase C.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Two files import this: `apps/web/auth.ts`, for Better Auth, and
|
|
9
|
+
* `lib/license-key-store.ts`, which hands `prismaForAuth.siteSetting` to the
|
|
10
|
+
* licence client's SiteSetting store for the ONE deployment-wide
|
|
11
|
+
* `license.renewed_key` row (#6677 DD-6 — a narrow, named exception).
|
|
12
|
+
* Every other business-logic caller goes through apps/api, never the
|
|
13
|
+
* database directly (architecture.yaml `apps/web` may_not_import_from
|
|
14
|
+
* `database`; the single-file singleton here is the documented exception,
|
|
15
|
+
* matching the pre-migration carve-out for NextAuth's adapter).
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
import { PrismaClient } from "@working-theory/database";
|
|
17
19
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
20
|
+
import { PrismaClient } from "@working-theory/database";
|
|
18
21
|
|
|
19
22
|
declare global {
|
|
20
23
|
var __ailk_prisma: PrismaClient | undefined;
|
|
@@ -109,7 +109,7 @@ export const SITE_BASE_URL =
|
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Shared brand mark — the square logomark (the central brand library's
|
|
112
|
-
* `logomark/loop/
|
|
112
|
+
* `logomark/loop/tile-black.svg`). The Nav pairs it with `/wordmark.svg`.
|
|
113
113
|
* RSC-safe (no hooks, no client state).
|
|
114
114
|
*/
|
|
115
115
|
export function siteLogo(): ReactElement {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// GENERATED by create-ailk from the declared template's `layout` block
|
|
4
4
|
// (#5612). A scaffold with no template, or a template with no `layout`
|
|
5
|
-
// block, gets
|
|
6
|
-
//
|
|
5
|
+
// block, gets the defaults below (the announcement banner is off unless a
|
|
6
|
+
// template turns it on, #6469). `apps/web/app/[locale]/layout.tsx` reads this to decide:
|
|
7
7
|
// - nav: "mark-only" → Nav renders the brand mark + right-hand controls
|
|
8
8
|
// only (no nav links).
|
|
9
9
|
// - footer: "centered" → Footer renders its existing centered variant.
|
|
@@ -24,6 +24,6 @@ export interface SiteChrome {
|
|
|
24
24
|
export const siteChrome: SiteChrome = {
|
|
25
25
|
nav: "full",
|
|
26
26
|
footer: "columns",
|
|
27
|
-
announcementBanner:
|
|
27
|
+
announcementBanner: false,
|
|
28
28
|
home: "default",
|
|
29
29
|
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site identity — reads `siteIdentity` out of `content/_site.mdx` and
|
|
3
|
+
* registers it with `@working-theory/page-renderer`, so every page's JSON-LD
|
|
4
|
+
* carries the site's entity graph: the identity node, its optional secondary
|
|
5
|
+
* (the product), and the `WebSite` (#6597).
|
|
6
|
+
*
|
|
7
|
+
* A mirror of `lib/site-theme.ts`: a local frontmatter parse (the
|
|
8
|
+
* `@working-theory/site-config/server` subpath is forbidden in `apps/web`,
|
|
9
|
+
* #1567), validated against the canonical `SiteIdentitySchema` from the
|
|
10
|
+
* package's client-safe `/schema` entry. `page-renderer` may not import
|
|
11
|
+
* site-config (`architecture.yaml`), so the identity is read here, once, and
|
|
12
|
+
* handed over through `setSiteIdentity`.
|
|
13
|
+
*
|
|
14
|
+
* Registration happens at module load. `app/[locale]/layout.tsx` imports this
|
|
15
|
+
* module, and Next.js evaluates a layout module before any page under it
|
|
16
|
+
* renders, so every `renderPage` / `pageToJsonLd` call sees the identity
|
|
17
|
+
* without a per-route prop.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
setSiteIdentity,
|
|
25
|
+
type SiteIdentityRegistration,
|
|
26
|
+
} from "@working-theory/page-renderer";
|
|
27
|
+
import { featureListFromSections } from "@working-theory/schema";
|
|
28
|
+
import { SiteIdentitySchema } from "@working-theory/site-config/schema";
|
|
29
|
+
import { parse as parseYaml } from "yaml";
|
|
30
|
+
|
|
31
|
+
import { resolveContentRoot } from "./site-brand";
|
|
32
|
+
|
|
33
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
34
|
+
|
|
35
|
+
/** The parsed frontmatter of one file, or `undefined` when it cannot be read. */
|
|
36
|
+
function readFrontmatter(path: string): Record<string, unknown> | undefined {
|
|
37
|
+
try {
|
|
38
|
+
const match = FRONTMATTER_RE.exec(readFileSync(path, "utf8"));
|
|
39
|
+
if (!match) return undefined;
|
|
40
|
+
const parsed: unknown = parseYaml(match[1] ?? "");
|
|
41
|
+
return parsed && typeof parsed === "object"
|
|
42
|
+
? (parsed as Record<string, unknown>)
|
|
43
|
+
: undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The product's `featureList`: the feature titles of the product's own page —
|
|
51
|
+
* the first `SoftwareProduct` seed in the default locale, in sorted order
|
|
52
|
+
* (#6658 D3) — projected by `featureListFromSections`. A site with no product
|
|
53
|
+
* page falls back to a page whose slug is `features` (any type directory,
|
|
54
|
+
* first match in sorted order). Empty when there is neither.
|
|
55
|
+
*/
|
|
56
|
+
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
|
+
// #6658 D3: the product's own detail page (the first `SoftwareProduct`
|
|
74
|
+
// seed in sorted order) is where the feature sections live now.
|
|
75
|
+
const productDir = join(contentRoot, locale, "SoftwareProduct");
|
|
76
|
+
let productSeeds: string[] = [];
|
|
77
|
+
try {
|
|
78
|
+
productSeeds = readdirSync(productDir)
|
|
79
|
+
.filter((name) => name.endsWith(".mdx"))
|
|
80
|
+
.sort();
|
|
81
|
+
} catch {
|
|
82
|
+
// No SoftwareProduct directory — fall through to the legacy probe.
|
|
83
|
+
}
|
|
84
|
+
for (const name of productSeeds) {
|
|
85
|
+
const frontmatter = readFrontmatter(join(productDir, name));
|
|
86
|
+
if (frontmatter !== undefined) return featuresOf(frontmatter);
|
|
87
|
+
}
|
|
88
|
+
let typeDirs: string[];
|
|
89
|
+
try {
|
|
90
|
+
typeDirs = readdirSync(join(contentRoot, locale)).sort();
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
for (const typeDir of typeDirs) {
|
|
95
|
+
const frontmatter = readFrontmatter(
|
|
96
|
+
join(contentRoot, locale, typeDir, "features.mdx"),
|
|
97
|
+
);
|
|
98
|
+
if (frontmatter !== undefined) return featuresOf(frontmatter);
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The site's identity registration, or `undefined` when `_site.mdx` is absent,
|
|
105
|
+
* unreadable, or declares no `siteIdentity`. An invalid `siteIdentity` throws,
|
|
106
|
+
* deliberately: a customer whose identity silently vanished from every page
|
|
107
|
+
* would have been lied to (the stance `site-theme.ts` takes on colours).
|
|
108
|
+
*/
|
|
109
|
+
export function readSiteIdentity(
|
|
110
|
+
contentRoot: string = resolveContentRoot(),
|
|
111
|
+
): SiteIdentityRegistration | undefined {
|
|
112
|
+
const frontmatter = readFrontmatter(join(contentRoot, "_site.mdx"));
|
|
113
|
+
if (frontmatter?.siteIdentity === undefined) return undefined;
|
|
114
|
+
|
|
115
|
+
const identity = SiteIdentitySchema.parse(frontmatter.siteIdentity);
|
|
116
|
+
const { siteName, baseUrl, defaultLocale } = frontmatter;
|
|
117
|
+
const registration: SiteIdentityRegistration = {
|
|
118
|
+
siteName: typeof siteName === "string" ? siteName : identity.name,
|
|
119
|
+
baseUrl: typeof baseUrl === "string" ? baseUrl : identity.url,
|
|
120
|
+
identity,
|
|
121
|
+
};
|
|
122
|
+
if (
|
|
123
|
+
identity.kind === "Organization" &&
|
|
124
|
+
identity.secondary?.kind === "SoftwareApplication"
|
|
125
|
+
) {
|
|
126
|
+
registration.featureList = readFeatureList(
|
|
127
|
+
contentRoot,
|
|
128
|
+
typeof defaultLocale === "string" ? defaultLocale : "en",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return registration;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setSiteIdentity(readSiteIdentity());
|
|
@@ -25,6 +25,7 @@ import { join } from "node:path";
|
|
|
25
25
|
|
|
26
26
|
import { SiteThemeSchema } from "@working-theory/site-config/schema";
|
|
27
27
|
import {
|
|
28
|
+
siteThemeShapeCss,
|
|
28
29
|
siteThemeToBrandTokens,
|
|
29
30
|
type BrandTokenOverrides,
|
|
30
31
|
} from "@working-theory/theme";
|
|
@@ -72,3 +73,13 @@ export function loadSiteBrandTokens(): BrandTokenOverrides | undefined {
|
|
|
72
73
|
const theme = SiteThemeSchema.parse(block);
|
|
73
74
|
return siteThemeToBrandTokens(theme, SOURCE);
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The site's shape settings (`theme.corners`, `theme.eyebrowCase`) as a CSS
|
|
79
|
+
* override block for `:root`. "" when the site sets neither.
|
|
80
|
+
*/
|
|
81
|
+
export function loadSiteShapeCss(): string {
|
|
82
|
+
const block = readThemeBlock();
|
|
83
|
+
if (block === undefined) return "";
|
|
84
|
+
return siteThemeShapeCss(SiteThemeSchema.parse(block));
|
|
85
|
+
}
|