diff --git a/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx b/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx index 9e50ceb19b..5e34eb0cd5 100644 --- a/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/blog/[[...slug]]/page.tsx @@ -3,6 +3,7 @@ import { blog } from '@/lib/source'; import { getMDXComponents } from '@/mdx-components'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/lib/layout.shared'; +import { absoluteUrl } from '@/lib/site'; import Link from 'next/link'; import { ArrowLeft } from 'lucide-react'; @@ -192,6 +193,9 @@ export async function generateMetadata({ return { title: 'Blog', description: 'Insights, updates, and best practices from the ObjectStack team.', + // The index has no MDX file behind it, so its route is spelled out here — the + // same literal `app/sitemap.ts` lists it under. + alternates: { canonical: absoluteUrl('/blog') }, }; } @@ -204,5 +208,6 @@ export async function generateMetadata({ return { title: page.data.title, description: page.data.description, + alternates: { canonical: absoluteUrl(page.url) }, }; } diff --git a/apps/docs/app/[lang]/docs/[[...slug]]/page.tsx b/apps/docs/app/[lang]/docs/[[...slug]]/page.tsx index 55d3a22d79..ed46b9888b 100644 --- a/apps/docs/app/[lang]/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/docs/[[...slug]]/page.tsx @@ -9,6 +9,7 @@ import { File, Folder, Files } from 'fumadocs-ui/components/files'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions'; import { gitConfig } from '@/lib/layout.shared'; +import { absoluteUrl } from '@/lib/site'; export default async function Page(props: { params: Promise<{ lang: string; slug?: string[] }>; @@ -63,5 +64,12 @@ export async function generateMetadata(props: { return { title: page.data.title, description: page.data.description, + /** + * `page.url` is the same locale-stripped route fumadocs uses for in-site links + * and that `app/sitemap.ts` lists, so the canonical link and the sitemap entry + * cannot drift apart. `absoluteUrl()` throws rather than emit a URL on another + * host if that ever stops being a site-relative path. + */ + alternates: { canonical: absoluteUrl(page.url) }, }; } diff --git a/apps/docs/app/[lang]/page.tsx b/apps/docs/app/[lang]/page.tsx index 7f20305e5c..c0b09e8dee 100644 --- a/apps/docs/app/[lang]/page.tsx +++ b/apps/docs/app/[lang]/page.tsx @@ -4,6 +4,7 @@ import { ArrowRight, Check } from 'lucide-react'; import { Bricolage_Grotesque, IBM_Plex_Mono } from 'next/font/google'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions, gitConfig } from '@/lib/layout.shared'; +import { absoluteUrl } from '@/lib/site'; import { YouTubeEmbed } from '@/components/youtube-embed'; /** The 90-second overview — the same video the README's hero cover links to. */ @@ -25,6 +26,13 @@ export const metadata: Metadata = { title: 'Metadata framework for AI-written apps', description: 'ObjectStack turns the whole app — data model, UI, workflows, permissions — into typed metadata: a complete CRM in under 150k tokens, one context window.', + /** + * `/` is the one indexable spelling of the homepage: `proxy.ts` rewrites `/` to + * this route internally, and the prefixed form `/en` 307s back to `/`. Every + * other spelling a crawler reaches it by — query strings, tracking parameters — + * points here. + */ + alternates: { canonical: absoluteUrl('/') }, }; const VOCABULARY: { tag: string; title: string; copy: string }[] = [ diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 16f5cc607a..0bc2cb10ae 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,8 +1,19 @@ import './global.css'; import type { ReactNode } from 'react'; import type { Metadata } from 'next'; +import { SITE_ORIGIN } from '@/lib/site'; export const metadata: Metadata = { + /** + * The origin every relative URL in this site's metadata resolves against — + * canonical links today, the Open Graph / Twitter image paths next. Left unset, + * Next resolves them against a build-time guess of the deployment's own origin, + * so a preview build would advertise itself as the real site. + * + * `new URL(...)` at the point of use, so `lib/site.ts` keeps exporting an + * immutable string rather than a `URL` instance shared across every route. + */ + metadataBase: new URL(SITE_ORIGIN), title: { template: '%s | ObjectStack', default: 'ObjectStack',