Next.js¶
@easeo/next returns a native Next.js Metadata object from generateMetadata.
Simple: a static page¶
// app/about/page.tsx
import { easeoMetadata } from "@easeo/next";
import type { Metadata } from "next";
export function generateMetadata(): Metadata {
return easeoMetadata({
entity: { entityType: "page", title: "About", description: "About us." },
route: "/about",
config: { canonicalHost: "example.com", publicBaseUrl: "https://example.com" },
});
}
export default function About() {
return <h1>About</h1>;
}
Complex: a dynamic product route¶
A shared config module keeps the site settings in one place. The page maps a product record to an entity, then lets overrides win for the social card.
// lib/seo.ts
import type { SEOConfig } from "@easeo/core";
export const config: SEOConfig = {
canonicalHost: "shop.example.com",
publicBaseUrl: "https://shop.example.com",
siteName: "Example Shop",
titleTemplate: "{title} - Example Shop",
defaultOgImage: "https://shop.example.com/assets/og-image.png",
publisherName: "Example Shop",
locale: "en_US",
twitterSite: "@exampleshop",
searchUrlTemplate: "https://shop.example.com/search?q={search_term_string}",
};
// app/products/[slug]/page.tsx
import type { Metadata } from "next";
import { easeoMetadata } from "@easeo/next";
import { config } from "@/lib/seo";
import { getProduct } from "@/lib/products";
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const product = await getProduct(params.slug);
return easeoMetadata({
entity: {
entityType: "product",
title: product.name,
description: product.shortDescription,
image: product.imageUrl,
imageWidth: 1200,
imageHeight: 630,
imageAlt: product.name,
sku: product.sku,
price: product.price,
priceCurrency: product.currency,
availability: product.inStock ? "InStock" : "OutOfStock",
updatedAt: product.updatedAt,
},
route: `/products/${product.slug}`,
config,
});
}
Complex: reuse the payload for a route handler¶
generateMetadata covers the head, but sometimes you also need the JSON-LD or
the hash for caching. Build the payload directly in a route handler:
// app/api/seo/products/[slug]/route.ts
import { NextResponse } from "next/server";
import { buildSeoPayload } from "@easeo/core";
import { config } from "@/lib/seo";
import { getProduct } from "@/lib/products";
export async function GET(_request: Request, { params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
const payload = buildSeoPayload(
{ entityType: "product", title: product.name, description: product.shortDescription },
`/products/${product.slug}`,
config
);
return NextResponse.json(
{ html: payload.renderHtml(), jsonLd: payload.schemaJsonLd },
{ headers: { ETag: payload.etag(), "Cache-Control": "public, max-age=300" } }
);
}
Notes¶
configis required; the core rejects an emptycanonicalHost.- Images are only set when the payload has one; otherwise Next.js omits the key.
- The same
SEOConfigobject works for botheaseoMetadataandbuildSeoPayload. - See the Next.js integration page for the returned
Metadatamapping.