The first version of InfiniteGrammar.de was designed in Lovable and built out with Claude Code. That combination was fast. I had a working product, an admin area and dozens of content pages in a short time, and none of it felt risky, because everything I clicked on worked.

The problem was in what I didn’t click on. The app was a React single-page application on Netlify. The browser received an empty shell and React drew the page. For a learner that’s fine. For a search engine deciding whether to index dozens of grammar pages, it is a weak signal unless you deliberately add the machinery to fix it. I added that machinery late, and in the wrong order.

The mistakes that mattered

Treating meta tags as the SEO task. My first instinct was titles, descriptions and social preview tags. They improved how links looked when shared. They did nothing for indexing, because the crawler still faced a client-rendered shell. A page can have a perfect title and still be a weak page for search.

Patching SEO on from outside. I tried scripts that injected metadata after the build instead of making prerendered pages the real output. That created several sources of truth: one in React, one in scripts, sometimes a third in the sitemap or redirect rules. Once several systems define the same URLs, they drift, and the drift caused real problems.

Changing URLs while being crawled. Every change of URL structure needed redirects, and repeated changes created redirect chains that wasted crawl budget and blurred the signals. URL stability isn’t an implementation detail. It is a product decision.

Ignoring trailing slashes. On a host that serves prerendered pages as folders, /page/ and /page are different: one returns the page, the other a redirect. When the sitemap, canonical tags, internal links and prerender list disagree on the slash, the site generates redirects to its own canonical pages.

Before: React routes, the prerender script, sitemap.xml and redirect rules are four lists edited separately, so they drift apart. After: one route inventory feeds the prerendered pages, the sitemap, canonical tags, internal links and redirect tests.
The fix in one picture. One route inventory, and everything else generated from it.

The turning point was making prerendering part of the build itself, with Puppeteer rendering about 120 routes into real HTML documents that React takes over after loading. It works, but it is still more fragile than a framework with built-in static generation. It slows the build and can drift from what React renders in the browser.

One more trap was outside the code entirely. Netlify’s post-processing features injected duplicate social tags taken from the base HTML, which appeared before the page’s own tags. Social crawlers take the first match, so link previews showed the wrong text even though the build was correct. Anything between your build output and the crawler is part of the SEO system.

What I’d choose today

For a content-heavy product that depends on search, I would start with a framework where every route produces HTML as a normal part of the build: Next.js, Astro or similar. If I stayed on React with Vite, I would set up the SEO system before launch, not after: one route inventory as the single source of truth, build-time prerendering for every indexable route, per-route canonical tags, a sitemap generated from the inventory, structured data for the main content types, robots rules separating public from private areas, one enforced trailing-slash policy, and redirect tests in the deployment checks.

The broader lesson is about AI-assisted building. Fast UI iteration hides architectural debt, because everything a human clicks on works. The question I should have asked on day one was simple: does this stack produce stable, crawlable HTML for every page I want indexed?

The rules I give a coding agent now

These go into the project instructions before an agent touches routing:

  • Treat SEO as a rendering problem first. Every public route that should rank gets prerendered HTML at build time.
  • Use one route inventory for the sitemap, prerender targets, canonical tags and internal links. Never maintain the sitemap by hand.
  • Don’t change URL structures unless necessary. If you must, use single-hop permanent redirects, and never link internally to a redirecting URL.
  • Trailing-slash consistency is mandatory, not cosmetic.
  • Fail the build if an indexable route has no prerendered output, or if the sitemap and the route list disagree.
  • After every deploy, fetch a sample of live URLs and compare them with the build output. If they differ, something between the two is changing the HTML.

Those rules later became the deterministic part of the SEO autopilot, where an agent has to prove each one with a frozen test.

The stack at a glance

PartTechnologyWhat I’d change
Learner app and adminReact 18, TypeScript, Vite, Tailwind, shadcn/ui, TanStack Query, Recharts, i18nextSplit the admin into its own app so its heavy charts stay out of the learner bundle
BackendNetlify Functions in TypeScript, raw SQLA light query builder for simple reads and writes; keep raw SQL for analytics
DatabaseNeon (serverless Postgres)Nothing; migrations now live in the generator repository with tests
EmailResendRemove the leftover second email library
Content pipelinePython, OpenAI Batch API, resumable runsDone since this was first written: verified pipeline, budgets, regression eval
Similarity analysisPython, spaCy, scikit-learn, SciPy; rented CPU on Vast.aiA scheduled run after every refill instead of a manual one

If you build with AI coding tools

  1. Ask the architecture questions the UI can’t answer: what does a crawler, a screen reader or a slow phone actually receive?
  2. Keep one source of truth for anything several systems need, such as URLs.
  3. Treat your hosting configuration as part of the product.
  4. Write your hard-won rules into the agent’s instructions, so you only learn each one once.