WordPress users have RankMath and Yoast. Next.js users have whatever they decide to build.
That is usually fine, because most Next.js sites have ten pages and their SEO needs are met by exporting the right metadata objects. It stops being fine once you have dozens of dynamic routes, content that changes weekly, and a growing list of URLs you have moved or removed.
At that point you are either editing code and redeploying for changes that are fundamentally editorial, or you are building a control panel. This is a walkthrough of the second option — the SEO tooling behind this site, why each piece exists, and the parts that turned out to be harder than expected.
The Problem With Code-Only SEO
Next.js gives you excellent primitives. The metadata API is well designed, next-sitemap handles generation, and JSON-LD is just a script tag. None of that is the issue.
The issue is the deployment cycle. Consider what routine SEO work actually looks like:
- You rename a blog post and need the old URL to redirect
- Search Console reports a page with a truncated meta description
- You want FAQ structured data on three specific pages to test rich results
- You need to temporarily deindex a page while you rewrite it
Every one of those is a content decision. None of them should require a code change, a review, and a deployment. The gap between "I noticed a problem" and "the fix is live" should be under a minute, and with a code-only approach it is closer to twenty.
Database-Driven Redirects at the Edge
This is the module I would build first in any content-heavy Next.js project.
Next.js supports a static redirects array in its configuration. It works, and it requires a rebuild for every change. For a handful of permanent rules that is entirely reasonable. For ongoing content operations it is not.
The alternative is storing redirects as records — a source path, a destination, a status code, an enabled flag — and resolving them in middleware at request time. Each record also tracks a hit counter and a last-hit timestamp, which turns out to be more useful than expected: it tells you which redirects are still carrying traffic and which are dead weight you can retire.
The performance problem is obvious once you write it down. Middleware runs on essentially every request. A database query per request to check whether a redirect exists would be indefensible.
The fix is a module-scope cache with a short time-to-live. The redirect list is fetched once, held in memory, and reused for the next thirty seconds. The overwhelming majority of requests match against an in-memory array with no I/O whatsoever, and an edit still goes live within half a minute without any deployment.
Two details worth stealing:
- Skip early. Middleware returns immediately for anything under the framework's internal paths, for API routes, and for anything with a file extension. There is no reason to check redirects for a stylesheet request.
- Keep the stale cache on failure. If the redirect lookup fails, serving a slightly outdated list is strictly better than failing the request. Redirect resolution should never be the reason a page does not load.
The 404 Monitor
This one takes an afternoon and is disproportionately useful.
The middleware attaches the requested path as a header on every pass-through response. The not-found page reads that header and records the request: path, referrer, user agent, country, a hit counter, and first and last seen timestamps. Records are unique by path, so repeat hits increment rather than duplicate.
The referrer is the whole point. Without it, a 404 log is a list of URLs that do not exist, which is mildly interesting. With it, each entry tells you a story:
- Referrer is your own domain — you have a broken internal link, which is entirely your fault and entirely fixable
- Referrer is an external site — someone is linking to you and the link is broken; that is inbound link equity leaking away, and a redirect recovers it
- Referrer is a search engine — a URL you removed is still indexed, and users are landing on nothing
- No referrer, odd paths — usually bots probing for vulnerable endpoints, safe to mark resolved and ignore
Search Console will eventually tell you about most of these. It will tell you weeks later, sampled, and aggregated. Your own log tells you today, completely.
Injectable Structured Data
Structured data is where I changed my mind partway through building.
My first instinct was to manage all JSON-LD from the database, on the theory that anything editorial should be editable. That turned out to be wrong in one specific way.
Schema that is intrinsic to a content type belongs in code. A blog post is always an Article. A service page always emits a Service object. A detail page always has a BreadcrumbList. These do not vary editorially — they vary with the shape of the template — and putting them next to the template that renders them keeps them correct automatically as the template evolves.
Schema that is experimental or page-specific belongs in the database. Testing whether an FAQ block earns rich results on three particular pages, adding an Organization object site-wide, trying a HowTo on one tutorial — these change based on what you are testing, not on how the page works.
So the system stores schema templates as records with a name, a type, the raw JSON-LD, and an attachment rule: either attach to every page, or attach to a specific list of paths. The layout resolves the site-wide ones and each page resolves its own. Templates validate their JSON on save, because invalid structured data is worse than none — it fails silently and you find out through a Search Console warning six weeks later.
The Sitemap Trap
Worth calling out because it is a mistake I made in this very project and only caught later.
The sitemap configuration originally contained a hardcoded list of service URLs, written in anticipation of pages that had not been built yet. The pages never got built. The sitemap kept dutifully submitting sixteen URLs that all returned 404.
That is worse than omitting them. You are spending crawl budget on nothing and handing Google a quality signal that works against you.
The lesson generalises: a sitemap should be generated from the same source of truth that generates the pages. If your pages come from the database, your sitemap should query the database. Any hardcoded list will eventually drift, and the drift is silent — nothing in your build fails, nothing in your tests catches it, and the only symptom is a slow decline in crawl efficiency.
The current configuration paginates through blogs, projects and services from their APIs and builds the URL list from what actually exists.
What Else Is In There
Several smaller modules that each solve one specific recurring annoyance:
- Title and meta templates — pattern strings with placeholders, so a change to how every project page phrases its title is one edit rather than a template change
- Image SEO rules — automatic alt text derived from titles with optional prefixes and suffixes, because missing alt attributes accumulate faster than anyone fixes them manually
- Robots.txt override — stored as text with a code fallback, so blocking a path is an edit rather than a deployment
- Webmaster verification tags — all the verification meta tags for various search engines in one settings record instead of scattered through the layout
- Instant indexing — IndexNow key management, so new content can be pushed to participating engines rather than waiting to be discovered
- Analytics and tracking IDs — every measurement script configured from one place, including arbitrary custom scripts with head or body placement
None of these are individually impressive. Collectively they mean that routine SEO work happens in a browser in under a minute instead of in an editor followed by a deploy.
Would I Recommend Building This?
Honestly, it depends on your situation.
If your site has ten static pages, no. Export the metadata objects, add next-sitemap, write your JSON-LD in the templates, and spend your time on the content instead. The tooling would cost more than it saves.
If you have dozens of dynamic routes, content that changes weekly, and URLs that move, then the calculation flips. Redirects and the 404 monitor alone justify the effort, and they are the two smallest modules. Everything after that is incremental.
The broader point is one I keep relearning: the value is rarely in any individual feature. It is in shortening the loop between noticing a problem and having it fixed. Tooling that turns a twenty-minute deploy cycle into a thirty-second edit changes not just how fast you fix things, but how many things you bother to fix at all.
Curious about the implementation, or building something similar? Get in touch — happy to talk through the details.

