DEV Community

Cover image for How I keep 500+ free dev tools in one Next.js App Router project
CoherenceDaddy
CoherenceDaddy

Posted on

How I keep 500+ free dev tools in one Next.js App Router project

Most "free dev tools" sites I've watched grow either stall at 3 tools — because every new one becomes a 4-hour yak-shaving session of routing, metadata, and SEO — or they explode into 50 separate repos that no human can maintain.

I wanted neither. I wanted one Next.js App Router project with 500+ tools, served fast, indexed cleanly, and shippable from a single git push. Today there are 523 tools live. Here's the architecture, the boilerplate, and the parts that broke.

The shape

The whole system is five places:

  • app/(tools)/<slug>/page.tsx — one tiny route file per tool
  • components/tools/<Name>.tsx — actual tool UI and logic
  • lib/tool-routes/<category>.ts — 19 category files, each exports a typed array
  • lib/tool-routes/index.ts — combines them into a single allTools
  • lib/tool-seo.tsgenerateToolMetadata() is the one source of truth

app/sitemap.ts and public/llms.txt both iterate over allTools at build time. Every new tool touches three places: a registry entry, a route file, and a component.

The per-tool boilerplate

This is what every route file looks like — five lines, identical except for the slug and the import:

import type { Metadata } from "next"
import { generateToolMetadata } from "@/lib/tool-seo"
import { MyTool } from "@/components/tools/MyTool"

export const metadata: Metadata = generateToolMetadata("my-tool")
export default function Page() { return <MyTool /> }
Enter fullscreen mode Exit fullscreen mode

Because everything else — title, description, keywords, canonical URL, OpenGraph image, Twitter card, JSON-LD — is derived from one entry in the registry, adding the 524th tool is a five-minute job, not a four-hour one. The friction stayed flat.

The registry, split by category

Initially I had one giant tool-routes.ts array. At ~80 tools it became unreviewable in PRs and TypeScript inference started visibly slowing the editor. I split it into 19 files by category — finance.ts, developer-tools.ts, crypto.ts, ai.ts, and so on — each exporting a typed array of ToolDef. Then tool-routes/index.ts does the spread:

const allTools = [...finance, ...developerTools, ...crypto, ...ai, ...etc]

The category split also gave the sidebar grouping for free, and made it possible to lazy-load category-specific data only on the relevant pages.

SEO at scale

The single biggest payoff of this architecture is SEO. Because every tool flows through the same metadata generator, I never have to remember to add a canonical tag or a Twitter card. The generator produces:

  • <title> and <meta description> from the registry entry
  • <link rel="canonical"> to the apex URL
  • OG image generated by next/og at the edge
  • JSON-LD SoftwareApplication schema per tool, plus a CollectionPage index
  • A BreadcrumbList derived from the category

Add a tool to the registry and it appears in the sitemap, the llms.txt, the AI plugin manifest, and the category sidebar, with no per-tool work. That feedback loop is what makes adding the 500th tool not feel like a chore.

What broke at scale

This is the part most "I built X" posts skip, and it's the part I want when I'm reading them.

Build times got slow around tool #200. Fix: audited per-route imports. Three tools were importing a 400KB chart library that I'd already replaced with CSS visualizations elsewhere. One was importing the entire lodash for a single debounce. Trimming saved about 90 seconds off the production build.

Sitemap hit Google's URL limits. A single sitemap.xml with 500+ entries wasn't the issue (the limit is 50K), but my sitemap also pulled from a directory subdomain and a partner directory, which together pushed it into uncomfortable territory for GSC's "Submitted, not yet processed" queue. Fix: per-subdomain sitemap files, each scoped to its host via headers().get('host') in app/sitemap.ts. Each subdomain's robots.txt declares its own.

LLM crawlers ate my bandwidth. Anthropic's ClaudeBot, OpenAI's GPTBot, Perplexity's PerplexityBot, and Google's GoogleOther were collectively making more requests than human users. I didn't want to block them — I want my tools cited — but I didn't want them re-crawling 500 pages every day either. Fix: a clean llms.txt and llms-full.txt at the root, plus an ai-plugin.json and mcp.json under .well-known/. The crawlers get a structured catalog, hit static text files instead of rendering 500 React pages, and my Vercel bandwidth bill stopped growing 15% week-over-week.

One regression broke 200 tools at once. I refactored the tool-routes/index.ts combiner to dedupe by slug. The dedupe logic was wrong, and 200 tools silently disappeared from the registry — still routable individually, but invisible in the index, sidebar, and sitemap. Caught it from a Vercel Analytics dip in tool-page traffic about a day later. Lesson: centralization is leverage in both directions, and you need at least a smoke test that asserts allTools.length >= 500 in CI.

What I'd do differently

Two things, in order of regret.

Define tool metadata in JSON, not TypeScript. I started in TS for the type safety. The cost was that every tool change became a code change, which means a code review, which means tools that should ship in 5 minutes ship in an afternoon. JSON with a Zod validator at the registry boundary would have given me the same safety with looser ergonomics.

Build the analytics from day one, not at #80. I have view counts now, popularity rankings, related-tool click-through. I should have had them from the first tool. Without per-tool analytics you have no data to answer "should I build the 502nd tool or improve one of the existing 501?" — and that's the only question that matters at scale.

Takeaway

The boring meta-lesson: at 500+ pages, your bottleneck isn't the building, it's the friction-per-page. Drive that to zero with a registry, one metadata generator, one sitemap loop, and accept that you're now operating a small infrastructure project, not a side hobby.

If you want to see the result, the live grid is at coherencedaddy.com. Built on Next.js 15 App Router, deployed on Vercel, with Neon Postgres + pgvector for the glossary semantic search.

Top comments (0)