Most technical SEO advice is framework-agnostic and therefore vague. This is the opposite: a list of things that break specifically on Next.js, all of which we have hit on production sites, and none of which produce an error message.
Work through it in order. The first four are the ones that cost real rankings.
1. Check what your output mode silently disables
This is the most expensive item on the list, so it goes first.
Setting output: 'export' in next.config.mjs produces a static site — and
silently discards three things:
| Discarded | What breaks |
|---|---|
redirects() | Every 301 you wrote. Old URLs 404 instead of passing equity |
rewrites() | Any proxying or URL masking |
headers() | Security headers, cache-control, everything |
There is no warning at build time and no error at runtime. The functions stay in your config, look correct in code review, and never run.
How to verify: request a URL you have a redirect for on the deployed site and check the status code. Then check for a header you configured:
curl -sI https://example.com/old-url/ | head -1
curl -sI https://example.com/ | grep -i strict-transport-security
If the redirect returns 200 or 404 rather than 301, and the header is absent,
your rules are not running. Either drop output: 'export' or move the rules to
your host's configuration. Do not leave them in a file where they read as active.
2. Route every page through one metadata helper
Next merges metadata one level deep. A page that exports this:
export const metadata = {
openGraph: { title: 'X', description: 'Y' },
};
does not add to the parent's openGraph — it replaces the whole object,
silently dropping images, type and siteName. The page still builds. The
page still looks fine. It just shares with no image.
The fix is not discipline, it is structure: one helper that reconstructs the full
object every time, and no page writing a raw metadata export.
Two related traps in the same area:
- A title
templatedefined in a layout applies to child segments, not to the segment that defines it. Your homepage title must be written in full. This is correct behaviour and it looks like a bug every single time. keywordsstill works and still does nothing. Google has ignored the meta keywords tag since 2009. Shipping it signals dated SEO to anyone reading your source.
3. Make trailing slashes agree everywhere
trailingSlash: true is a good default — it is unambiguous and it matches what
most hosts do. But it has to be matched by every URL you emit:
- Canonical tags
- Sitemap entries
- JSON-LD
@idvalues andurlproperties - Internal
hrefs - Any hardcoded URL in copy
A canonical of /services/seo on a site serving /services/seo/ points at a URL
that redirects. Search engines treat that as a weaker signal than a canonical
pointing at a live URL, and you get no warning that it is happening.
The reliable fix is a single absolute() helper used by everything that
constructs a URL, so the rule exists in one place rather than in the memory of
whoever writes the next page.
Most teams find the audit straightforward and stall on the remediation — the config changes, the redirect map, the canonical rewrite. That is the bulk of
what our technical SEO work covers.4. Server-render your navigation
Google renders JavaScript, so client-only navigation is not fatal for Googlebot. It is still worth fixing, because navigation is how internal link equity moves around your site, and several things that read your HTML do not render at all — social preview crawlers, many AI answer-engine crawlers, and most SEO tools.
The specific failure: a dropdown that only mounts on hover or click. In the server-rendered HTML, those links do not exist. Every page behind that dropdown loses its inbound links from every page on the site.
Ship the full menu in the HTML and hide it with CSS. Not conditional rendering — CSS.
opacity-0 invisible → group-hover:visible group-focus-within:visible
On this site, that single change took inbound internal links on deep service pages from 1–3 up to 21. It sits alongside the rest of the internal-linking work in the write-up of how that architecture was built, including the generated link graph that replaced hand-written arrays and the flaw in the first version of it.
5. Verify the sitemap against reality
Next's sitemap.ts generates from whatever you tell it to. Two things go wrong:
Routes it cannot see. If content lives outside the app directory — MDX
files, a CMS, anything — a directory walk will not find it. Those pages exist,
are linked, and are missing from the sitemap.
URLs that do not match canonicals. Usually the trailing slash problem from §3, appearing in a second place.
Check the deployed sitemap and count:
curl -s https://example.com/sitemap.xml | grep -c "<loc>"
Compare that number to the pages you believe you have. A mismatch is either a missing route or an orphan.
6. Decide what your crawl surface actually is
Faceted filters and pagination are where a content site quietly generates thousands of thin URLs.
- Filters: if a filter changes the URL, each combination is a crawlable page. Five types across eight categories with pagination is several hundred near-duplicate URLs, all competing with each other. Client-side filtering avoids this entirely at the cost of shareable filter links, which nobody shares.
- Pagination: give each page a self-referencing canonical. Canonicalising page 3 to page 1 tells search engines the content on page 3 does not exist.
7. Check cache headers on HTML, not just assets
Next stamps statically prerendered HTML with s-maxage=31536000 — a one-year
shared-cache lifetime. If your CDN honours it, a deploy can leave stale pages
served for days while your origin is already correct.
Hashed assets under /_next/static are immutable and should keep a long cache.
HTML should not. A short s-maxage with a long stale-while-revalidate keeps
the CDN useful while guaranteeing a deploy propagates in minutes.
8. Verify against the deployed site, not the dev server
Everything above passes on localhost. Most of it breaks at the host or CDN
layer, which the dev server does not have.
One trap worth knowing: some hosts run a WAF that challenges non-browser clients
with a 403 carrying noindex. A naive curl then reports a catastrophically
broken site that is in fact fine — and, more importantly, real AI crawlers can hit
the same challenge, because they are more likely to look non-browser-like than
Googlebot is.
Send browser-like headers when you check, and cache-bust every request. A stale CDN copy will otherwise show you a problem you fixed three deploys ago.
The short version
If you only do four things: check what output disabled, centralise metadata,
make trailing slashes agree, and server-render your navigation. Those four cover
most of the ranking damage a Next.js site does to itself.
Working through this list?
Most teams get through the audit and stall on the remediation. That is the part we usually take on.