Next.js SEO: What Actually Moves the Needle

Rebuilt your site in React and watched traffic drop? Here's what Google actually sees, and the three fixes that matter more than any plugin.
Your Old Site Isn't Slow Because It's Old. It's Slow Because Nobody's Touched the Foundation.
Every few months a client lands in my inbox with the same story, just wearing a different framework. This time it isn't a React rebuild that tanked their rankings — it's the opposite direction. They've got a ten-year-old WordPress install groaning under forty plugins, or a PHP site someone's cousin built in 2014, or a pile of static HTML pages that were hand-copied so many times nobody remembers which one has the correct meta description anymore. Traffic has been flat or sliding for years, and everyone assumes the fix is "more content" or "more backlinks." It's usually neither.
Open the page source on these sites and the story is almost always the same: bloated HTML tables from a page builder, three different jQuery versions loading on every page whether they're needed or not, a <title> tag that says "Home" on every single page, and images that are 4MB JPEGs served at 300px wide. Google can crawl this fine — that's not the problem old sites have. The problem is everything else: load time, duplicate or missing metadata, no structured sharing data, and a codebase so tangled that fixing one plugin conflict breaks two others.
Rebuilding these in Next.js isn't about chasing a trendy framework. It's about getting three things right that the old stack made structurally difficult to get right at all.
1. Rendering that doesn't fight the CMS
Old PHP and WordPress sites already render server-side HTML, which is good — but it's usually wrapped in so much unnecessary markup and blocking JavaScript that the actual content takes seconds to become usable. Migrating to Next.js with static generation or incremental static regeneration keeps the good part (HTML on first load) and strips the bad part (plugin bloat, render-blocking scripts, database queries running on every request).
Common issues I find in legacy migrations:
- WordPress pages making 15+ database queries per load because of stacked plugins
- Static HTML sites with copy-pasted
<head>blocks that drift out of sync page to page - PHP includes that pull in unused CSS/JS on every route regardless of what the page needs
- No caching layer, so every visitor triggers a full server render from scratch
Moving content into Next.js with generateStaticParams for blog posts, product pages, or service pages means the HTML is built once and served instantly, with ISR handling updates without a full redeploy.
2. Metadata that was never structured in the first place
This is where old sites lose the most ground quietly. WordPress SEO plugins often can do this right, but half the installs I audit have them misconfigured, disabled, or fighting with a second SEO plugin someone installed and forgot about. Static HTML sites are worse — metadata is usually copy-pasted once and never touched again.
The fix isn't complicated, it's just centralized. One component, driven by real data, used on every page:
// components/Seo.jsx
// Works as a drop-in wrapper for any page — Next.js App Router or Pages Router.
// For App Router, prefer exporting generateMetadata() per page and reuse buildMeta() below.
export function buildMeta({
title,
description,
keywords = [],
url,
image = '/og-default.jpg',
siteName = 'Your Site Name',
twitterHandle = '@yourhandle',
type = 'website',
}) {
const fullTitle = title ? `${title} | ${siteName}` : siteName;
return {
title: fullTitle,
description,
keywords: keywords.join(', '),
alternates: {
canonical: url,
},
openGraph: {
title: fullTitle,
description,
url,
siteName,
images: [{ url: image, width: 1200, height: 630, alt: title }],
type, // 'website' | 'article' | 'product'
// LinkedIn reads Open Graph tags directly, no separate tags needed
},
twitter: {
card: 'summary_large_image',
title: fullTitle,
description,
images: [image],
site: twitterHandle,
},
};
}
// Usage in an App Router page (app/blog/[slug]/page.js):
// export async function generateMetadata({ params }) {
// const post = await getPost(params.slug);
// return buildMeta({
// title: post.title,
// description: post.excerpt,
// keywords: post.tags,
// url: `https://yoursite.com/blog/${post.slug}`,
// image: post.coverImage,
// type: 'article',
// });
// }For teams migrating a Pages Router or a non-Next.js React frontend still mid-transition, the same object works with next/head:
// components/SeoHead.jsx (Pages Router / legacy React fallback)
import Head from 'next/head';
import { buildMeta } from './Seo';
export default function SeoHead(props) {
const meta = buildMeta(props);
return (
<Head>
<title>{meta.title}</title>
<meta name="description" content={meta.description} />
<meta name="keywords" content={meta.keywords} />
<link rel="canonical" href={meta.alternates.canonical} />
<meta property="og:title" content={meta.openGraph.title} />
<meta property="og:description" content={meta.openGraph.description} />
<meta property="og:url" content={meta.openGraph.url} />
<meta property="og:image" content={meta.openGraph.images[0].url} />
<meta property="og:type" content={meta.openGraph.type} />
<meta property="og:site_name" content={meta.openGraph.siteName} />
<meta name="twitter:card" content={meta.twitter.card} />
<meta name="twitter:title" content={meta.twitter.title} />
<meta name="twitter:description" content={meta.twitter.description} />
<meta name="twitter:image" content={meta.twitter.images[0]} />
<meta name="twitter:site" content={meta.twitter.site} />
</Head>
);
}One function, called per page, fed by real content instead of a plugin's best guess. Facebook and LinkedIn both read the og: tags, so there's no separate LinkedIn-specific markup needed — get Open Graph right once and both platforms unfurl correctly.
3. Speed that a plugin can't patch
Old sites accumulate speed problems the way a house accumulates clutter — nobody adds it all at once, it just piles up over years of "quick fixes." A caching plugin here, an image optimization plugin there, none of them talking to each other. Migrating gives you a chance to fix this structurally instead of patching it:
- Replace raw
<img>tags withnext/image, which handles resizing, lazy loading, and modern formats automatically - Cut unused plugin CSS/JS — most WordPress themes ship 10x more than any given page uses
- Move third-party scripts (analytics, chat widgets, tracking pixels) to load after the page is interactive, not before
- Audit fonts — legacy sites frequently load 4–6 font weights from multiple providers when two would do
None of this requires new content or a link-building budget. It's fixing a foundation that was never built for how search engines — or AI answer engines, which crawl even less patiently — actually read a page today.
I've taken sites from a 25 PageSpeed score to the 90s and watched organic traffic climb back within two months, without touching a word of the existing content. The old site wasn't wrong. It just needed a structure underneath it that Google and every crawler after it can actually read quickly and clearly. The same neglected stacks that lose rankings are usually one outdated plugin away from getting hacked too — a rebuild is a good moment to fix both.
Once the frontend is on Next.js, the backend is worth a second look — I run this exact site on my own self-hosted Firebase alternative instead of paying per-request to a third party.
Running an old PHP, WordPress, or static site and not sure what's holding it back? Send me your URL via contact and I'll show you exactly what's slowing it down and what a Next.js remodel would fix.
Related Posts
Related Articles

Scaling WebSocket Connections: What Breaks Past a Few Thousand Concurrent Users
WebSockets don't scale like REST APIs, they're stateful, long-lived connections, and that single difference breaks chat and notifications past a few thousand concurrent users. A breakdown of what actually needs to change: shared pub/sub state, connection-aware load balancing.

From 0 to 100K Users: Handling Users Smoothly from the Mobile App Client to the Backend
What actually breaks as a Flutter app scales isn't the UI — it's how sessions, token refresh, and concurrent writes are handled between the client and backend. A technical breakdown of the user-lifecycle failures that show up between 1K and 100K users.

API Security Checklist for SaaS Builders
Most SaaS breaches aren't zero-days, they're missing auth checks, leaked keys, and endpoints nobody remembered to lock down. Here's the checklist that actually prevents them.
