Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

I built a random Pokémon generator with Next.js and PokéAPI — here's what I learned about SEO in 2026

A few weeks ago I shipped PokeRoll, a free random pokemon generator: one tap rolls 1 of 1,000+ Pokémon with stats, abilities and artwork. It also builds a random team of 6 (with gen/region/type filters), runs a shiny hun

I built a random Pokémon generator with Next.js and PokéAPI — here's what I learned about SEO in 2026


A few weeks ago I shipped PokeRoll, a free random pokemon generator: one tap rolls 1 of 1,000+ Pokémon with stats, abilities and artwork. It also builds a random team of 6 (with gen/region/type filters), runs a shiny hunt at real 1/4096 odds, fuses two Pokémon, and flips every card into a copy-paste-ready Pokémon Showdown set.

This post isn't about the game side, though. It's about what happened when I took SEO seriously on a small static-ish site in 2026 — including the parts of "SEO" that only exist because AI search engines now read your pages too.

The stack: boring on purpose

  • Next.js App Router + ISR. Every page is statically generated and revalidated in the background. After full static-ization, TTFB dropped from ~2.25s to ~100ms. For a site whose entire SEO strategy is "rank for hundreds of long-tail generator keywords," cheap and fast beats clever.
  • PokéAPI as the only data source. It gives you game-level data (base stats, types, moves). One thing it does not have: recommended EV spreads — that's competitive-community knowledge, not game data. I initially went looking for it in the API before realizing the distinction. We ended up with a documented heuristic (252/252/4) instead of integrating an external dataset. Lesson: before wiring up a data source, classify what you're looking at — game data, community knowledge, or your own convention.
  • Programmatic routes for long-tail. /type/[type] × 18 types, /gen/[n] × 9 generations, /by/[region] × 9 regions, all from templates. This is where the SEO fun starts.

Lesson 1: Intent matching doesn't count. Verbatim coverage does.

My first pass at keywords was vibes-based: "the page is obviously about that, Google will figure it out." It won't.

The fix was a three-step audit:

  1. Map every high-frequency query to a page. I dumped the keyword list from Google autocomplete/competitor titles into a text file, then mapped each one to a landing page — enumerating all dynamic route parameters (18 types × 9 gens × 9 regions).
  2. Sort the gaps into three buckets:
    • Copy gap — page exists, keyword doesn't → add it to metadata immediately. Cheapest win there is.
    • Feature gap — keyword needs a feature that doesn't exist (dual-type generator, etc.) → roadmap it.
    • Wrong intent — emulator/ROM/TCG queries → deliberately drop. Not the same product.
  3. Verify with a script, not your eyes. After editing, I grepped every keyword against the generated metadata of its page. This caught real bugs — e.g. my random ${type} pokemon generator template was missing the literal word "type" for one variant, which no human review would have spotted.

Lesson 2: Title real estate is front-loaded

  • Put the high-frequency query at the front of the title. My homepage title used to lead with the brand; the main keyword sat in the middle. Flipped it.
  • Standardize the suffix as keyword | Brand. I deleted a "— Fan-made Tool" suffix: 16 characters carrying zero ranking value.
  • Gotcha worth sharing: I batch-replaced the title suffix across the codebase with replace-all… and the same string appeared in a regex literal used to strip the suffix. The new suffix contained |, which in a regex means "or". Everything silently broke. Before a bulk replace, grep the string in code logic, not just in copy.

Lesson 3: Meta description is a CTR game with a hard window

140–160 characters is the window checkers enforce — too short wastes the snippet, too long gets truncated. Easy for static pages, genuinely fiddly for templates:

  • 18 type names differ by ~5 characters; 9 region game-name pairs differ by ~14. A fixed sentence won't fit all parameter values.
  • Fixes: conditional filler (when the Kalos game name "X & Y" makes the template too short, append "instantly") and length-tiered templates (the share-card description picks one of two templates based on Pokémon name length).
  • Validate by counting real output for every parameter value, min and max — never estimate.
  • Description doesn't rank you; it earns the click. Write a call to action ("copy it to Showdown"), not a summary.

Lesson 4: GEO — optimizing for AI search engines is its own checklist

I ran an AI-search-readiness audit (ChatGPT / Perplexity / AI Overviews citation readiness). What actually moved the needle:

  • llms.txt at the root: site intro, key URLs, data source, contact.
  • FAQPage JSON-LD generated from the same FAQ component that renders the visible page — one source of truth, so the schema can't drift from the content.
  • Real citations only. For "Citations & Quotations" I fetched Bulbapedia's shiny-odds page and PokéAPI's homepage with curl and quoted them verbatim in <blockquote> + <cite>. Don't paraphrase a quote, and never invent one — this took me two audit rounds to pass precisely because round one had links without actual quotations.
  • Structured content means tables and lists. A grid of <div> cards doesn't count. I added a <table> of numbers and an <ol> getting-started list.
  • Author & freshness signals: author + datePublished (first git commit date) + dateModified (refreshed on ISR revalidation) in the WebPage schema, plus a visible byline.
  • Heading hierarchy bugs hide in component libraries — my Pokémon name was an <h3> jumping levels under an <h1>. When you fix the tag, remember the CSS selectors bound to it.

Lesson 5: Distribution is part of shipping

Technical SEO gets you eligible to rank. What gets you into the top 10 is backlinks and user behavior. My current playbook: open-source repo with keyword-rich anchor text, community posts (Reddit/Tumblr), a Product Hunt launch, and share-card URLs that canonical back to the main domain so every shared link consolidates authority instead of fragmenting it across tracking parameters.

One underrated trick: my external links carry ?r=<channel> params for attribution, and every page has a self-referencing canonical — so link equity from all those parameterized URLs rolls up to the clean URL.

The code is open

The whole site is on GitHub: ihuajiu/pokeroll.app — Next.js App Router, ISR, zero backend, zero sign-up. If this was useful, a star helps more than you'd think.

What's your experience with programmatic SEO on small sites? And has anyone actually measured whether llms.txt does anything yet? Genuinely curious.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.