Next.js & technical SEO · June 30, 2026 · 15 min read · Updated June 30, 2026
Next.js Static vs Dynamic Rendering: Which Should a Marketing Page Use?
By Tal Gerafi, Founder & Website Engineer
In short
Almost every marketing page in Next.js should render statically (SSG), because pre-built HTML gives the fastest TTFB, the cleanest crawl, and the lowest cost. Use ISR when content changes on a schedule but doesn't need to be per-request fresh. Reserve dynamic SSR for genuinely personalized or request-specific pages, which marketing rarely is. The decision rule is simple: static by default, ISR for scheduled freshness, dynamic only when the response truly depends on the request. This guide covers the rendering decision, App Router SEO setup, the complete B2B schema graph, the Yoast features you rebuild by hand, real index-hygiene war stories, and a copy-paste checklist.
For a marketing page in Next.js, render statically by default. Pre-built HTML gives the fastest time-to-first-byte, the cleanest crawl, and the lowest cost — exactly what a brochure-style page needs. Reach for incremental static regeneration when content changes on a schedule, and for server-side rendering only when the response genuinely depends on the request. Marketing pages almost never do.
This guide gives you the decision rule, the App Router SEO setup that replaces what WordPress did for free, the complete schema graph a B2B site needs, the index-hygiene traps we've hit and fixed, and a checklist you can paste into a project today.
Static vs dynamic rendering in Next.js: what's the difference?
Static rendering builds a page's HTML once, at build time, and serves that same file to every visitor and crawler. Dynamic rendering builds the HTML fresh on each request, on the server. The static-vs-dynamic-rendering split is the single biggest performance-and-SEO decision you make per route in Next.js.
Static — static site generation, or SSG — produces a file that a CDN serves instantly from the edge. There's no server work per request, so time-to-first-byte is near-zero and a crawler always gets fully-formed HTML. Dynamic — SSR — runs your component on the server for every hit, which lets the page reflect the current request but adds latency and cost to every page view. Between them sits ISR: pages are static, but Next.js rebuilds them in the background on a schedule or on demand, so you get static speed with controlled freshness.
In the App Router, a route is static by default and only opts into dynamic rendering when you use request-time APIs (cookies(), headers(), searchParams) or set dynamic = 'force-dynamic'. That default is a gift for marketing sites: do nothing special and you already get the fast, crawlable version. The mistakes happen when a stray request-time call quietly tips a page into dynamic rendering and nobody notices the TTFB regression.
Which rendering strategy should a marketing page use?
Use this rule: static by default, ISR for scheduled freshness, dynamic only when the response truly depends on the request. A homepage, a product page, a pricing page, an about page, a blog post — all static. A blog index that updates when you publish — ISR. A logged-in dashboard or a live search results page — dynamic. Marketing pages live almost entirely in the first two buckets.
The test is one question: does this page need to change based on who is asking or when they ask, in a way a build can't predict? If no, it's static. A pricing page that changes when you edit it is still static — you rebuild on deploy. A "latest posts" list is the borderline case, and ISR handles it cleanly: render it statically, revalidate every few minutes, and never pay SSR's per-request cost.
| Page type | Strategy | Why |
|---|---|---|
| Homepage, about, features | Static (SSG) | Content changes on deploy, not per request |
| Pricing, product, landing pages | Static (SSG) | Same HTML for everyone; speed and crawl win |
| Blog post / guide | Static (SSG) | Write once, serve forever from the edge |
| Blog index, "latest" lists | ISR | Static speed, refreshes when you publish |
| Docs that sync from a CMS | ISR (on-demand revalidate) | Rebuild the changed page, not the whole site |
| Logged-in dashboard, account | Dynamic (SSR) | Output depends on the user |
| Live search, real-time data | Dynamic (SSR) | Output depends on the request |
If you're moving off WordPress, this is also a migration decision — see the WordPress to Next.js migration SEO guide, because picking the wrong rendering mode during a rebuild can quietly slow every page you just "modernized."
SSG vs SSR vs ISR for SEO: a comparison table
For SEO, static (SSG) wins on the metrics that matter most: it has the lowest TTFB, the most reliable crawlability, and the lowest cost, because the HTML already exists before the crawler arrives. SSR can match crawlability but pays latency on every request. ISR is the pragmatic middle — static delivery with freshness on a timer.
TTFB matters for SEO in two ways. It feeds Core Web Vitals (a slow server response delays Largest Contentful Paint), and a fast, stable response keeps your crawl budget efficient — when Googlebot fetches pages quickly and predictably, it crawls more of your site per visit. SSR pages that time out under load get crawled more conservatively.
| Dimension | Static (SSG) | ISR | Dynamic (SSR) |
|---|---|---|---|
| TTFB | Lowest (served from CDN edge) | Lowest (served static, rebuilt in background) | Higher (server runs per request) |
| Content freshness | On deploy only | On a timer or on-demand | Every request |
| Server cost | Near-zero | Low | Scales with traffic |
| Crawlability | Excellent — full HTML, always | Excellent — full HTML, always | Good, but latency-sensitive |
| Failure mode | Stale until rebuild | Briefly stale window | Slow / 5xx under load |
| Best for | Brochure, blog, landing | Lists, CMS-fed content | Personalized, request-specific |
The practical takeaway: a B2B marketing site that ships everything static, with ISR on its index pages, gets the best of every column except real-time freshness — which it doesn't need. For more depth on the trade-off, see static vs dynamic rendering in Next.js.
How do you set up App Router SEO: generateMetadata, sitemap, robots, canonicals?
App Router SEO has four load-bearing pieces: generateMetadata for per-page titles, descriptions, and canonicals; a sitemap.ts that generates your XML sitemap from real data; a robots.ts for crawl rules; and a metadataBase plus self-referencing canonical so every page declares its one true URL. None of these exist by default — you build them once and they cover the whole site.
Wire them in this order. First, set metadataBase and a title template in the root layout so every relative canonical and OG image resolves to an absolute URL. Then add a self-referencing canonical per page with generateMetadata:
// app/layout.tsx
export const metadata = {
metadataBase: new URL('https://greeto.studio'),
title: { default: 'Greeto Studio', template: '%s · Greeto Studio' },
alternates: { canonical: '/' },
}
// app/guides/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const doc = await getGuide(slug)
return { title: doc.seoTitle, description: doc.metaDescription,
alternates: { canonical: `/guides/${slug}` } } // metadataBase makes it absolute
}
Then generate the sitemap from your content source (never hand-maintained, never stale) and ship a robots.ts that allows the crawlers you want — including AI crawlers — and points to it:
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const guides = await getAllGuides()
return guides.map((g) => ({ url: `https://greeto.studio/guides/${g.slug}`,
lastModified: g.updatedAt, changeFrequency: 'monthly', priority: 0.8 }))
}
// app/robots.ts
export default function robots(): MetadataRoute.Robots {
return { rules: { userAgent: '*', allow: '/' },
sitemap: 'https://greeto.studio/sitemap.xml' }
}
Lock your trailing-slash policy in next.config so /guides/x and /guides/x/ never both resolve, and make sure your canonical matches that exact form. One canonical shape, declared everywhere, is what stops the index-hygiene problems later in this guide.
What is the complete schema graph for a B2B marketing site?
The complete schema graph wires your entities together by @id so search and answer engines read them as one connected knowledge object, not four loose snippets. The minimum for a B2B site: an Organization, a Person (founder or key author), a WebSite, the Service(s) you sell, and FAQPage blocks — each referencing the others by URL-based @id. The wiring is the part most guides skip, and it's exactly what Greeto ships.
Most sites paste an Organization block and a separate FAQPage block and call it done. The engine sees two unrelated things. The fix is @id references: the Service points to its provider (the Organization), the Organization names its founder (the Person), the WebSite declares its publisher, and a BreadcrumbList ties pages into the hierarchy. Now it's a graph — one entity vouching for the next.
| Entity | Role on a B2B site | Key links (by @id) |
|---|---|---|
Organization | The company itself | founder → Person; sameAs → profiles |
Person | Founder / named author (E-E-A-T) | worksFor → Organization |
WebSite | The site as a whole | publisher → Organization |
Service | What you sell | provider → Organization |
FAQPage | Q&A on a page | lives on the page; cite-ready |
BreadcrumbList | Page hierarchy | itemListElement → pages |
A trimmed example of the wiring:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "https://greeto.studio/#org",
"name": "Greeto Studio", "founder": { "@id": "https://greeto.studio/#tal" } },
{ "@type": "Person", "@id": "https://greeto.studio/#tal",
"name": "Tal Gerafi", "worksFor": { "@id": "https://greeto.studio/#org" } },
{ "@type": "Service", "@id": "https://greeto.studio/#service",
"provider": { "@id": "https://greeto.studio/#org" } }
]
}
This graph is citation infrastructure for AI answers as much as rich-result markup — see how to rank in ChatGPT and Perplexity for why connected entities get cited, and the complete schema graph for a Next.js B2B site for the full implementation.
Which Yoast features do you rebuild by hand in Next.js?
WordPress with Yoast handed you titles, meta descriptions, canonicals, XML sitemaps, robots rules, Open Graph tags, breadcrumbs, and schema — all generated automatically. Next.js gives you none of these by default. You rebuild each one explicitly: more work up front, but no plugin drift and total control over the output. The mapping is one-to-one, and most of it is the App Router setup above:
| Yoast did this automatically | In Next.js you build |
|---|---|
| SEO title + template | title + template in metadata |
| Meta description | description in generateMetadata |
| Canonical URL | alternates.canonical + metadataBase |
| XML sitemap | app/sitemap.ts from your content |
robots.txt / noindex | app/robots.ts + per-page robots metadata |
| Open Graph / Twitter cards | openGraph / twitter in metadata |
| Breadcrumbs | BreadcrumbList JSON-LD component |
| Schema (Org, Article, FAQ) | The @graph JSON-LD above |
llms.txt (newer Yoast/AI plugins) | A hand-shipped llms.txt route |
The upside of doing it by hand: your schema is exactly what you decide, your canonicals follow one rule, and nothing changes because a plugin updated overnight. The risk is forgetting a piece — which is why the checklist below exists. The migration-specific version of this work lives in the WordPress to Next.js migration SEO guide.
Index-hygiene war stories: trailing slashes, canonicals, and cannibalization
The three index-hygiene traps we hit most are inconsistent trailing slashes, canonicals that point at the wrong URL, and two pages competing for the same query (cannibalization). Each is invisible in a browser and obvious in Search Console weeks later. All three are fixable, and all three are cheaper to prevent than to clean up.
The trailing-slash one is the classic. If /guides/x and /guides/x/ both resolve with 200, you've split one page into two URLs — duplicate content, split signals, and a sitemap that may list the form your canonical disagrees with. The fix: pick one policy, enforce it in next.config, redirect the other form with a 301, and make every canonical and sitemap entry use that exact shape. We've had to undo this after the fact, and reconciling the sitemap, canonicals, and redirects at once is far more annoying than setting one rule on day one.
Canonical mistakes are quieter. A templated canonical that hardcodes the homepage path, or one that survives a migration still pointing at the old domain, tells Google to consolidate signals onto the wrong URL. In our experience the cause is almost always a copy-paste canonical that wasn't made page-specific — which is why a self-referencing canonical derived from the route, not a constant, is the safe default. Cannibalization is the third: a glossary term and a guide both chasing the identical phrase, so neither ranks cleanly. The cure is intent separation — the glossary defines, the guide teaches, the blog argues a point of view — with internal links that make the hierarchy obvious. The lesson across all three: clean rendering doesn't save you from messy URLs. For the redirect side, see the WordPress to Next.js redirect map.
How does rendering affect Core Web Vitals?
Rendering choice mostly drives the server-response portion of Core Web Vitals. Static pages serve pre-built HTML from a CDN, so TTFB is near-zero and Largest Contentful Paint starts as early as possible. Dynamic (SSR) pages add server compute to every request, pushing TTFB up and LCP back. Rendering doesn't directly set Cumulative Layout Shift or Interaction to Next Paint — your layout and JavaScript do — but a slow server start makes every other metric harder to hit.
Think of it as a budget: every millisecond your server spends building HTML is a millisecond LCP can't start. Static spends zero of that budget per request; SSR spends it every time. ISR keeps the static budget while refreshing content in the background, so visitors and crawlers get an instant response even right after a rebuild.
| Web Vital | How rendering affects it |
|---|---|
| TTFB | Static near-zero; SSR adds per-request server time |
| LCP | Earlier on static (HTML ready instantly) |
| CLS | Layout/CSS-driven; rendering mode is mostly neutral |
| INP | JS-driven; ship less client JS regardless of mode |
So rendering is necessary but not sufficient: static gives you the best possible starting line, then your images, fonts, and JavaScript decide whether you keep the lead. Motion-heavy marketing pages especially need to watch this — see motion performance metrics that actually matter and motion without jank.
The copy-paste Next.js SEO checklist
Run this before any marketing site goes live. It assumes the App Router setup above and covers rendering, metadata, schema, and index hygiene in the order they bite. Each line is a yes/no you can verify.
- Rendering — Every marketing page is static (SSG) or ISR; none is accidentally dynamic. Confirm in the build output.
- Canonicals — Each page emits a self-referencing canonical derived from its route, absolute via
metadataBase. None points at the homepage or an old domain. - Trailing slash — One policy in
next.config; the other form 301-redirects; canonicals and sitemap use the chosen form. - Metadata — Unique
titleanddescriptionper page viagenerateMetadata;openGraph/twitterimages resolve absolutely. - Sitemap —
app/sitemap.tsgenerated from real content, listing only indexable, canonical URLs. - Robots —
app/robots.tsallows the crawlers you want (including AI crawlers) and points to the sitemap; noindex on thin or utility pages. - Schema graph — One
@graphwiring Organization → Person → WebSite → Service, plusFAQPageandBreadcrumbList, joined by@id. Validate it. - llms.txt — Shipped and reachable, summarizing the site for answer engines.
- Index hygiene — No two pages chase the identical query; intent separated across glossary, guide, and blog.
- Core Web Vitals — TTFB low (static delivery), LCP image preloaded, client JS trimmed.
Run this before launch and again after any big content or routing change. For the AI-search layer on top, the AEO playbook for B2B websites extends it into citation territory.
FAQ
Should every marketing page be statically rendered in Next.js?
Almost always, yes. Brochure pages, product and pricing pages, blog posts, and landing pages all serve the same HTML to everyone, so static rendering gives the fastest response and the cleanest crawl with no downside. Use ISR for pages that list frequently-changing content, and reserve dynamic SSR for pages whose output genuinely depends on the individual request, which marketing pages rarely are.
Is ISR better than SSR for SEO?
For most content sites, yes. ISR serves pre-built static HTML from the edge — so TTFB and crawlability match pure static — while rebuilding pages on a schedule or on demand to stay fresh. SSR rebuilds on every request, adding latency and cost without an SEO benefit unless the page must reflect each request live. Use ISR when you want freshness without paying SSR's per-request tax.
Does static vs dynamic rendering affect Google rankings?
Indirectly but meaningfully. Google ranks content, not rendering mode, but static rendering produces faster server responses (better TTFB and LCP) and more reliable crawls, both of which support ranking. Dynamic pages that respond slowly or fail under crawl load can be crawled less thoroughly. So rendering doesn't rank you by itself — it removes the speed and crawl obstacles that would otherwise hold you back.
What replaces Yoast SEO when you move to Next.js?
You rebuild Yoast's features explicitly: generateMetadata for titles, descriptions, and canonicals; app/sitemap.ts for the XML sitemap; app/robots.ts for crawl rules; openGraph/twitter for social cards; and hand-written JSON-LD for schema and breadcrumbs. There's no single drop-in plugin — but doing it in code means no plugin drift and full control over the exact output.
What is a schema graph and why does it matter for B2B?
A schema graph connects your structured-data entities — Organization, Person, WebSite, Service, FAQPage — using @id references, so engines read them as one knowledge object instead of disconnected snippets. For B2B it matters because connected entities establish who you are, who stands behind the work, and what you sell, which strengthens both rich results and your odds of being cited in AI answers.
How do I stop two Next.js pages from cannibalizing the same keyword?
Separate intent and consolidate signals. Give each page a distinct job — a glossary term defines, a guide teaches, a blog post argues a view — so they target related but different queries. Then link them in a clear hierarchy and make sure canonicals are self-referencing and correct. If two pages still compete, merge them or point one's canonical at the stronger page.
How do I know if a Next.js page is rendering statically or dynamically?
Check the build output. After next build, Next.js marks each route as static, ISR (revalidating), or dynamic. If a page you expected to be static shows as dynamic, you've likely used a request-time API (cookies(), headers(), or searchParams) or set dynamic = 'force-dynamic' somewhere in its tree. Remove or isolate that call to push the route back to static.
Glossary terms in this guide