A static export
Next renders it into the <head>. There is no <Head> component and no next/head in the App Router — metadata is data, exported from the route.
export const metadata = {
title: "Doors",
description: "Made to order, in eight weeks.",
alternates: { canonical: "https://domvybe.com/doors" },
};
export default function Page() { … }
The title template
The template applies to the segments below the layout that defined it, never to the layout itself — which is what default is for.
// app/layout.tsx
export const metadata = {
title: {
default: "DOMVYBE — doors made to order", // when a page sets nothing
template: "%s · DOMVYBE", // when a page sets a title
},
};
// app/doors/page.tsx
export const metadata = { title: "Doors" }; // → "Doors · DOMVYBE"
// and to escape the pattern
export const metadata = { title: { absolute: "DOMVYBE" } }; // → "DOMVYBE"
Dynamic metadata
export async function generateMetadata({ params }) {
const { id } = await params;
const door = await getDoor(id); // deduplicated with the page's own fetch
if (!door) return { title: "Not found" };
return {
title: door.name,
description: door.summary,
openGraph: { images: [door.image] },
};
}
Do not fetch twice
The page and generateMetadata usually need the same record. fetch is deduplicated for you; a database call is not — wrap it in cache() or you have doubled every query on the route.
The social card
- 1200 × 630 is the size everything renders well at.
- Without
metadataBase, relative image URLs are dropped — a card with no image is the usual symptom. - Generate the image if it is per-page:
opengraph-image.tsxbeside the route renders one with JSX at request time.
export const metadata = {
metadataBase: new URL("https://domvybe.com"), // makes relative URLs absolute
openGraph: {
title: "Painted door",
description: "Five days, made to order.",
url: "/doors/painted",
siteName: "DOMVYBE",
images: [{ url: "/og/painted.png", width: 1200, height: 630 }],
type: "website",
},
twitter: { card: "summary_large_image" },
};
The file conventions
app/
favicon.ico
icon.png → <link rel="icon">
apple-icon.png
opengraph-image.png → the card image for this segment
opengraph-image.tsx → or generate it with JSX
robots.txt / robots.ts
sitemap.xml / sitemap.ts
A sitemap that is generated
export default async function sitemap() {
const doors = await getDoors();
return [
{ url: "https://domvybe.com", lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
...doors.map((d) => ({
url: `https://domvybe.com/doors/${d.id}`,
lastModified: d.updatedAt,
changeFrequency: "monthly",
priority: 0.8,
})),
];
}
What actually matters for search
- A unique title and description per page. Duplicates across a site are worse than a missing one.
- A canonical URL, especially when filters and tracking parameters produce the same page at many addresses.
- Real HTML: a server-rendered page is indexable, a client-only one is a gamble.
- Speed, on a phone. Core Web Vitals are a ranking signal and a real experience.
- And nothing here helps a page with nothing worth reading on it.