Skip to main content
All documentation

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.