Every framework has an SEO story now, and almost all of them are the same story: a helper that writes tags. Next has Metadata, Nuxt has useHead, Astro has whatever you import this month. They are fine. They are also the least interesting part of the problem, because writing a was never what broke anyone's search traffic.
The interesting question is a different one: what does the framework do when you do nothing at all?
A metadata API is opt-in by definition. It helps on the pages you remembered. The failures that actually cost traffic are the ones nobody remembered, because nothing errored — a 200 status on a page that doesn't exist, a canonical URL pointing at http://, a link to a page the build silently skipped. None of those throw. None of them show up in a test. They show up in Search Console eleven weeks later.
If a competent developer builds a content site and never once thinks about search engines, what is already correct?
I want to work through the categories that answer that question, using Stoneware as the worked example — partly because I built it, and mostly because its defaults were chosen against exactly this test. Every code sample and every output below is real. The document has to be complete in the first response
This is the foundation, and it is upstream of everything else. If your content is assembled on the client, then everything downstream — canonical tags, structured data, sitemaps — is decoration on a page a crawler may or may not fully see.
The honest version of this claim is measurable, not rhetorical. Here is a 21-route content site, built three ways from byte-identical content:
Stoneware and Astro send a document and nothing else. Next sends 576 KB of JavaScript to a page with no interactive element on it, and re-encodes the article a second time inside the HTML as an RSC payload.
Stoneware's rule here is structural rather than advisory: a file under routes/ is never handed to the bundler, so it cannot reach the client. Interactivity is opt-in per directory — a component under islands/ hydrates, everything else is a string the server produced.
What a framework should handle automatically: rendering to complete HTML by default, with client JavaScript as the exception you ask for. Status codes, and the soft 404
This is the most common indexing bug on content sites and almost nobody talks about it.
A dynamic route matches any slug. /blog/[slug] happily matches /blog/asdfghjkl. Your template then looks up the post, doesn't find it, and — if you're not careful — renders "Post not found" with a 200 OK.
That page is now indexable. Google will crawl it, and it may index it. Worse, the pattern generates infinitely many of them.
The fix has to be ergonomic or people won't use it. In Stoneware it's one call that throws:
Because it throws rather than returns, it works from a helper three calls deep without every function in between having to pass a sentinel back up. And because its return type is never, TypeScript narrows post to present afterwards — no non-null assertion.
Two details worth stealing regardless of framework: Error responses are no-store. A 404 cached by a CDN outlives the deploy that adds the missing page. That's a self-inflicted outage with a long tail. A leading underscore means a file isn't servable. Without that rule, routes/404.tsx would answer a real request at /404 with a 200 — your error page, indexable as content.
What a framework should handle automatically: making the correct status the easy one, and never serving its own conventions as pages. Canonical URLs that survive a reverse proxy
Every platform that terminates TLS — Render, Railway, Fly, Vercel, nginx — forwards a plain HTTP request to your app. So new URL(request.url) reports http:// for a site served over https://. Anything absolute you build from it is now wrong: pointing at http:// og:image on the insecure origin sitemap entries on the wrong scheme OAuth redirect URIs that don't match
