Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
.next/
.test-dist/
out/
.env
.env.local
Expand Down
3 changes: 2 additions & 1 deletion app/compliance/page.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
7 changes: 7 additions & 0 deletions app/contracts/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="section">
Expand Down
7 changes: 7 additions & 0 deletions app/contributors/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="section">
Expand Down
7 changes: 7 additions & 0 deletions app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="section">
Expand Down
30 changes: 22 additions & 8 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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" }],
Expand All @@ -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 (
<html lang="en">
<body className={inter.variable}>
{/* Serialized by serializeJsonLd, which escapes '<' to prevent script injection. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(structuredData) }}
/>
<header className="nav">
<div className="container nav-inner">
<Link href="/" className="brand brand-with-logo">
Expand All @@ -82,3 +95,4 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</html>
);
}
/* eslint-enable react/no-danger */
3 changes: 2 additions & 1 deletion app/operators/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { getRouteDescription } from "@/lib/site-map";
import { Suspense } from "react";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { PanelLoading } from "./components/PanelStates";
Expand All @@ -9,7 +10,7 @@ import SettlementsPanel from "./components/SettlementsPanel";

export const metadata: Metadata = {
title: "Operators",
description: "Operator dashboard — live testnet data and quota headroom.",
description: getRouteDescription("/operators"),
};

export default function Page() {
Expand Down
6 changes: 6 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { Metadata } from "next";
import Link from "next/link";
import { BrandLogo } from "@/components/brand-logo";
import { ExpectedPages } from "@/components/expected-pages";
import { getRouteDescription } from "@/lib/site-map";

export const metadata: Metadata = {
description: getRouteDescription("/"),
};

export default function HomePage() {
return (
Expand Down
3 changes: 2 additions & 1 deletion app/product/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { getRouteDescription } from "@/lib/site-map";

export const metadata: Metadata = {
title: "Product",
description: "Verifiable usage, dispute-free settlement, and immutable audit trails.",
description: getRouteDescription("/product"),
};

function PlannedTag() {
Expand Down
7 changes: 7 additions & 0 deletions app/roadmap/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import type { Metadata } from "next";
import { getRouteDescription } from "@/lib/site-map";

export const metadata: Metadata = {
description: getRouteDescription("/roadmap"),
};

export default function Page() {
return (
<section className="section">
Expand Down
21 changes: 21 additions & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { MetadataRoute } from "next";
import { getSiteUrl, isPreviewDeployment } from "../lib/site";

export default function robots(): MetadataRoute.Robots {
if (isPreviewDeployment()) {
return {
rules: {
userAgent: "*",
disallow: "/",
},
};
}

return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: new URL("/sitemap.xml", getSiteUrl()).toString(),
};
}
11 changes: 11 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MetadataRoute } from "next";
import { publicRoutes } from "../lib/site-map";
import { getSiteUrl } from "../lib/site";

export default function sitemap(): MetadataRoute.Sitemap {
const siteUrl = getSiteUrl();

return publicRoutes.map(({ path }) => ({
url: new URL(path, siteUrl).toString(),
}));
}
17 changes: 9 additions & 8 deletions components/expected-pages.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { publicRoutes } from "../lib/site-map";

export function ExpectedPages() {
return (
<section className="section site-map" id="site-map">
Expand All @@ -17,14 +19,13 @@ export function ExpectedPages() {
</tr>
</thead>
<tbody>
<tr key="/"><td><code>/</code></td><td>Marketing hub + site map</td><td>Scaffold</td></tr>
<tr key="/product"><td><code>/product</code></td><td>Personas, pricing hooks, integration story</td><td>Planned</td></tr>
<tr key="/contracts"><td><code>/contracts</code></td><td>Soroban modules and interaction flows</td><td>Planned</td></tr>
<tr key="/operators"><td><code>/operators</code></td><td>Dashboard preview for AI gateways</td><td>Planned</td></tr>
<tr key="/compliance"><td><code>/compliance</code></td><td>Audit exports and policy packs</td><td>Planned</td></tr>
<tr key="/roadmap"><td><code>/roadmap</code></td><td>Milestones vs grants</td><td>Scaffold</td></tr>
<tr key="/contributors"><td><code>/contributors</code></td><td>Good first issues and guild roles</td><td>Planned</td></tr>
<tr key="/docs"><td><code>/docs</code></td><td>Technical reference hub</td><td>Scaffold</td></tr>
{publicRoutes.map(({ path, purpose, status }) => (
<tr key={path}>
<td><code>{path}</code></td>
<td>{purpose}</td>
<td>{status}</td>
</tr>
))}
</tbody>
</table>
</div>
Expand Down
63 changes: 63 additions & 0 deletions lib/site-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export const publicRoutes = [
{
path: "/",
purpose: "Marketing hub + site map",
status: "Scaffold",
description:
"Learn how ModelTrace turns AI usage into attested facts on-chain for procurement, finance, and audit teams.",
},
{
path: "/product",
purpose: "Personas, pricing hooks, integration story",
status: "Planned",
description: "Verifiable usage, dispute-free settlement, and immutable audit trails.",
},
{
path: "/contracts",
purpose: "Soroban modules and interaction flows",
status: "Planned",
description: "Explore ModelTrace Soroban modules and interaction flows.",
},
{
path: "/operators",
purpose: "Dashboard preview for AI gateways",
status: "Planned",
description: "Operator dashboard — live testnet data and quota headroom.",
},
{
path: "/compliance",
purpose: "Audit exports and policy packs",
status: "Planned",
description: "Verifiable lineage, GDPR-compliant data residency, and deterministic evidence.",
},
{
path: "/roadmap",
purpose: "Milestones vs grants",
status: "Scaffold",
description: "Track ModelTrace milestones, protocol releases, and grant checkpoints.",
},
{
path: "/contributors",
purpose: "Good first issues and guild roles",
status: "Planned",
description: "Find ModelTrace good first issues and contributor guild roles.",
},
{
path: "/docs",
purpose: "Technical reference hub",
status: "Scaffold",
description: "Read ModelTrace technical specifications, governance, and integration guides.",
},
] as const;

export type PublicPath = (typeof publicRoutes)[number]["path"];

export function getRouteDescription(path: PublicPath): string {
const route = publicRoutes.find((candidate) => candidate.path === path);

if (!route) {
throw new Error(`Unknown public route: ${path}`);
}

return route.description;
}
4 changes: 4 additions & 0 deletions lib/site-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function resolveSiteUrl(env: SiteUrlEnv = process.env): string {
return normalizeUrl(env.NEXT_PUBLIC_SITE_URL.trim());
}

if (env.VERCEL_PROJECT_PRODUCTION_URL?.trim()) {
return normalizeUrl(env.VERCEL_PROJECT_PRODUCTION_URL.trim());
}

if (env.VERCEL_URL?.trim()) {
return normalizeUrl(env.VERCEL_URL.trim());
}
Expand Down
12 changes: 12 additions & 0 deletions lib/site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { resolveSiteUrl } from "./site-url";

export const SITE_NAME = "ModelTrace";
export const SITE_DESCRIPTION = "Verifiable AI inference accounting on Stellar.";

export function getSiteUrl(): URL {
return new URL(resolveSiteUrl());
}

export function isPreviewDeployment(): boolean {
return process.env.VERCEL_ENV === "preview";
}
29 changes: 29 additions & 0 deletions lib/structured-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SITE_DESCRIPTION, SITE_NAME, getSiteUrl } from "./site";

const ORGANIZATION_NAME = "FinesseStudioLab";
const ORGANIZATION_URL = "https://github.com/FinesseStudioLab";

export function getStructuredData() {
return {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": ORGANIZATION_URL,
name: ORGANIZATION_NAME,
url: ORGANIZATION_URL,
},
{
"@type": "SoftwareApplication",
name: SITE_NAME,
url: getSiteUrl().toString(),
description: SITE_DESCRIPTION,
publisher: { "@id": ORGANIZATION_URL },
},
],
};
}

export function serializeJsonLd(value: unknown): string {
return JSON.stringify(value).replace(/</g, "\\u003c");
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
"test:e2e:ui": "playwright test --ui",
"test:seo": "tsc -p tsconfig.test.json && node --test .test-dist/tests/seo.test.js"
},
"dependencies": {
"next": "^15.1.3",
Expand Down
Loading