From f8b15754c02f13f1dffd0a766ce6598cb87fefc6 Mon Sep 17 00:00:00 2001 From: VitorBernalRodrigues Date: Fri, 21 Aug 2026 11:16:18 -0400 Subject: [PATCH] feat: add SEO metadata and structured data --- .gitignore | 1 + app/compliance/page.tsx | 7 +++ app/contracts/page.tsx | 7 +++ app/contributors/page.tsx | 7 +++ app/docs/page.tsx | 7 +++ app/layout.tsx | 29 ++++++--- app/operators/page.tsx | 7 +++ app/page.tsx | 6 ++ app/product/page.tsx | 7 +++ app/roadmap/page.tsx | 7 +++ app/robots.ts | 21 +++++++ app/sitemap.ts | 11 ++++ components/expected-pages.tsx | 17 +++--- lib/site-map.ts | 63 ++++++++++++++++++++ lib/site.ts | 19 ++++++ lib/structured-data.ts | 29 +++++++++ package.json | 3 +- tests/seo.test.ts | 107 ++++++++++++++++++++++++++++++++++ tsconfig.test.json | 16 +++++ 19 files changed, 353 insertions(+), 18 deletions(-) create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 lib/site-map.ts create mode 100644 lib/site.ts create mode 100644 lib/structured-data.ts create mode 100644 tests/seo.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index 1055e6d..2c3bc13 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 938df9f..db11021 100644 --- a/app/compliance/page.tsx +++ b/app/compliance/page.tsx @@ -1,3 +1,10 @@ +import type { Metadata } from "next"; +import { getRouteDescription } from "@/lib/site-map"; + +export const metadata: Metadata = { + description: getRouteDescription("/compliance"), +}; + export default function Page() { return (
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 0875230..580c01b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,26 +1,31 @@ import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; +import { getSiteUrl, isPreviewDeployment, SITE_DESCRIPTION, SITE_NAME } from "@/lib/site"; +import { getStructuredData, serializeJsonLd } from "@/lib/structured-data"; import "./globals.css"; export const metadata: Metadata = { - metadataBase: new URL("http://localhost:3000"), + metadataBase: getSiteUrl(), 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 }, openGraph: { - title: "ModelTrace", - description: "Verifiable AI inference accounting on Stellar.", + title: SITE_NAME, + description: SITE_DESCRIPTION, 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" }], @@ -39,9 +44,15 @@ const nav = [ ] as const; export default function RootLayout({ children }: { children: React.ReactNode }) { + const structuredData = getStructuredData(); + return ( + " }).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" + ] +}