The Next.js integration
Install the Dynamic SEO package in a Next.js App Router site, choose between the head component and the generateMetadata path, and read the verify step correctly.
Next.js is the second platform you can install today. The package is a download, the wiring is a handful of lines, and one decision matters: whether your site runs Cache Components. That decides which of two paths you take.
Before you start
The package is not on npm. The Integrate page's Next.js guide has the download, and its steps are printed with your own domain and the start of your site key. Everything here is the same guide with the reasoning attached.
The package ships TypeScript source. Next has to be told to compile it, which
is the one line every site needs in next.config:
const nextConfig = { transpilePackages: ['@dynamic-seo/middleware'] }
1. Download and install
Put the tarball in your repository, for example under vendor/, and install
it from that path. A committed file is what survives a clean build on your
host; a URL to a moving artifact does not.
npm install ./vendor/dynamic-seo-nextjs.tgz
2. The site key and the three variables
Copy the key from step 2 of the guide. It goes into .env locally and into
your host's environment variables for production, together with the edge
worker's URL and your domain as registered in Dynamic SEO:
DYNAMIC_SEO_WORKER_URL=https://dynamic-seo-edge.dynamicseo.workers.dev
DYNAMIC_SEO_DOMAIN=example.com
DYNAMIC_SEO_SITE_KEY=sk_...
The worker is the same edge the WordPress plugin talks to. A Next.js site therefore shows up in the Delivery card the same way.
3. Choose your path
Without Cache Components: the head component. Create proxy.ts (on Next 15
and earlier middleware.ts, exported as middleware) so the component knows
the current path, and render the component once inside body of the root
layout that wraps your public pages:
// proxy.ts
export { dynamicSEOMiddleware as proxy } from '@dynamic-seo/middleware/nextjs'
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|api/).*)'] }
// app/layout.tsx
import { DynamicSEOHead } from '@dynamic-seo/middleware/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<DynamicSEOHead
apiUrl={process.env.DYNAMIC_SEO_WORKER_URL!}
domain={process.env.DYNAMIC_SEO_DOMAIN!}
siteKey={process.env.DYNAMIC_SEO_SITE_KEY}
skipPaths={['/admin', '/api']}
/>
</body>
</html>
)
}
It renders title, description, canonical, Open Graph, Twitter and JSON-LD for
the current path, and nothing at all when nothing is published for it. If you
already have a proxy, wrap it with withDynamicSEO instead of exporting ours.
With Cache Components (cacheComponents: true in next.config): the head
component fails the build. It reads request headers, and Cache Components
forbids that outside a Suspense boundary. Skip the proxy and the layout, and
merge the published SEO into each page's own metadata instead. The path comes
from the page's params, the fetch sits behind use cache, and the static shell
is prerendered with the tags in it:
// lib/dynamicseo.ts
import type { Metadata } from 'next'
import { cacheLife } from 'next/cache'
import { createDynamicSEO } from '@dynamic-seo/middleware/nextjs'
const dseo = createDynamicSEO({
apiUrl: process.env.DYNAMIC_SEO_WORKER_URL!,
domain: process.env.DYNAMIC_SEO_DOMAIN!,
siteKey: process.env.DYNAMIC_SEO_SITE_KEY,
})
export async function withDynamicSeo(path: string, fallback: Metadata): Promise<Metadata> {
'use cache'
cacheLife('minutes')
return dseo.metadata(path, fallback)
}
// app/[slug]/page.tsx, and the same in every page that has metadata
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return withDynamicSeo(`/${slug}`, { title: 'Your own title' })
}
When nothing is published for a path, the fallback is what renders. This path is verified with a real build on a Next 16 site with Payload and Cache Components on.
4. Verify, and what the answer means
The verify step asks the edge for your homepage with your site key, from our side. Three answers are possible:
- Verified: the edge served published content for the homepage. The pipeline is live.
- Key works, nothing published yet: the edge answered, but there is nothing published for the homepage. That is the normal state right after install. Publish a page and run the test again.
- Key rejected: the key your installation sends is not this site's current key. Copy the current one from the guide, or regenerate and update every install.
Above the button, the step also says what your own site has done: whether it has called the edge, how many calls the last day got content, and when it was last seen. That is the serve log, the same numbers as the Delivery card on the dashboard, so a working install reads as working before anything is published.
Where to look when a page does not change
Work backwards. Is anything published for that path? Did your deploy pick up
the environment variables? Does the page's head contain
<meta name="dynamic-seo" content="active">? That tag is written on every
served response, so its absence means the request never got content, and the
Delivery card says why.
Read next
URLs and mapping