diff --git a/.gitignore b/.gitignore index a2fca90..782d940 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ .next/ +.test-dist/ out/ .env .env.local diff --git a/app/compliance/page.tsx b/app/compliance/page.tsx index 6331ec3..42c82b7 100644 --- a/app/compliance/page.tsx +++ b/app/compliance/page.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; +import { getRouteDescription } from "@/lib/site-map"; export const metadata: Metadata = { title: "Compliance", - description: "Verifiable lineage, GDPR-compliant data residency, and deterministic evidence.", + description: getRouteDescription("/compliance"), }; export default function CompliancePage() { diff --git a/app/contracts/page.tsx b/app/contracts/page.tsx index 8b4cd26..1baf6ac 100644 --- a/app/contracts/page.tsx +++ b/app/contracts/page.tsx @@ -1,3 +1,10 @@ +import type { Metadata } from "next"; +import { getRouteDescription } from "@/lib/site-map"; + +export const metadata: Metadata = { + description: getRouteDescription("/contracts"), +}; + export default function Page() { return (
diff --git a/app/contributors/page.tsx b/app/contributors/page.tsx index 301b35f..0cf95d3 100644 --- a/app/contributors/page.tsx +++ b/app/contributors/page.tsx @@ -1,3 +1,10 @@ +import type { Metadata } from "next"; +import { getRouteDescription } from "@/lib/site-map"; + +export const metadata: Metadata = { + description: getRouteDescription("/contributors"), +}; + export default function Page() { return (
diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 07197ba..e730dd6 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -1,3 +1,10 @@ +import type { Metadata } from "next"; +import { getRouteDescription } from "@/lib/site-map"; + +export const metadata: Metadata = { + description: getRouteDescription("/docs"), +}; + export default function Page() { return (
diff --git a/app/layout.tsx b/app/layout.tsx index 00500b6..1e40ec4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,9 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import Image from "next/image"; import Link from "next/link"; +import { isPreviewDeployment, SITE_DESCRIPTION, SITE_NAME } from "@/lib/site"; import { resolveSiteUrl } from "@/lib/site-url"; +import { getStructuredData, serializeJsonLd } from "@/lib/structured-data"; import "./globals.css"; const siteUrl = resolveSiteUrl(); @@ -17,25 +19,28 @@ const inter = Inter({ export const metadata: Metadata = { metadataBase: new URL(siteUrl), title: { - default: "ModelTrace", - template: "%s | " + "ModelTrace", + default: SITE_NAME, + template: `%s | ${SITE_NAME}`, }, - description: "Verifiable AI inference accounting on Stellar.", - applicationName: "ModelTrace", + description: SITE_DESCRIPTION, + applicationName: SITE_NAME, + robots: isPreviewDeployment() + ? { index: false, follow: false } + : { index: true, follow: true }, alternates: { canonical: "/", }, openGraph: { - title: "ModelTrace", - description: "Verifiable AI inference accounting on Stellar.", + title: SITE_NAME, + description: SITE_DESCRIPTION, url: "/", type: "website", locale: "en_US", }, twitter: { card: "summary_large_image", - title: "ModelTrace", - description: "Verifiable AI inference accounting on Stellar.", + title: SITE_NAME, + description: SITE_DESCRIPTION, }, icons: { icon: [{ url: "/icon.svg", type: "image/svg+xml" }], @@ -53,10 +58,18 @@ const nav = [ ["Docs", "/docs"], ] as const; +/* eslint-disable react/no-danger -- JSON-LD is serialized with '<' escaped before injection. */ export default function RootLayout({ children }: { children: React.ReactNode }) { + const structuredData = getStructuredData(); + return ( + {/* Serialized by serializeJsonLd, which escapes '<' to prevent script injection. */} + " }).includes("<"), false); +}); + +test("every public route has a unique description wired to its page", () => { + const descriptions = new Set(publicRoutes.map(({ description }) => description)); + + assert.equal(descriptions.size, publicRoutes.length); + + for (const route of publicRoutes) { + assert.ok(route.description.length > 0); + + const pagePath = + route.path === "/" ? "app/page.tsx" : `app${route.path}/page.tsx`; + const pageSource = readFileSync(path.join(process.cwd(), pagePath), "utf8"); + + assert.match(pageSource, new RegExp(`getRouteDescription\\(\\"${route.path}\\"\\)`)); + } +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..2332801 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": false, + "module": "commonjs", + "moduleResolution": "node", + "noEmit": false, + "outDir": ".test-dist" + }, + "include": [ + "app/robots.ts", + "app/sitemap.ts", + "lib/**/*.ts", + "tests/**/*.ts" + ] +}