Skip to main content
Back to Blog

3 min read

Scaling to 190 tools with a data-driven content model

How Astro's Content Layer API turns adding a new tool into a mechanical task — write an MDX file, wire an island, and everything else happens automatically.

astro content-layer architecture mdx

This site ships 192 interactive developer tools. Adding a new one takes about an hour. Not because the tools are simple — some have complex parsers, crypto, or multi-format converters — but because the content pipeline makes everything outside the core logic automatic.

The pipeline

Every tool is an MDX file with structured frontmatter:

---
title: "Cron Parser"
description: "Parse and explain cron expressions"
category: "web"
tags: ["cron", "schedule"]
type: "interactive"
island: "CronParser"
publishedAt: 2026-05-25
---

Astro’s Content Layer validates this against a Zod schema at build time. If a field is missing or the wrong type, the build fails before anything ships.

From that single MDX file, the system derives:

  • A page at /tools/web/cron-parser via [...slug].astro
  • A grid card on the /tools index, sorted and filterable by category
  • An OG image at /og/tools-web-cron-parser.svg, generated from the title
  • A sitemap entry for search engines
  • Search index data for the client-side tool search

No manual registration in a config file. No separate route definition. One getCollection('tools') call powers all of it.

What this costs to add a tool

When the cron parser, URL inspector, and Markdown preview shipped together in v1.2, the total wiring work — beyond the tools themselves — was:

  • 3 MDX files (frontmatter + description prose)
  • 3 import lines in the route file
  • 3 conditional renders mapping island names to components

The grid picked them up. The OG generator rendered them. The search indexed them. The sitemap included them. None of that code changed.

Why getCollection is the key abstraction

Astro’s getCollection('tools') returns every MDX file in the tools directory as a typed array. Every downstream consumer — the index page, the route generator, the OG pipeline — starts from that same array. There’s no intermediate registry, no manifest file, no build plugin.

This means the content is the configuration. Want to feature a tool? Set featured: true in its frontmatter. Want to reorder the grid? Change sortOrder. Want to add a new category? Just use a new category string — the grid filters derive categories from the data.

The route file is the only manual step

The one place that breaks the automation is src/pages/tools/[...slug].astro. Each interactive tool needs a conditional render that maps its island frontmatter value to the actual React component:

{data.island === 'CronParser' && <CronParser client:only="react" />}

This is intentional. Dynamic component imports in Astro would work, but they’d pull every island into the build graph. The explicit mapping keeps bundles small — each page only ships the JavaScript for its own tool.

After 192 tools, this file has 192 import lines and 192 conditionals. It’s mechanical, it’s ugly, and it’s the right trade-off. Build performance and bundle size matter more than DRY aesthetics in a route file nobody reads.

What scales and what doesn’t

Scales well: adding tools, changing metadata, reorganizing categories, generating derived assets (OG, sitemap, RSS). All of these are data operations on the collection.

Scales acceptably: the route file. It’s long but grep-friendly. Adding a tool means adding two lines in predictable locations.

Would need rethinking at 500+ tools: build time. Each MDX file is parsed and validated at build. At 192 tools, builds take ~4 seconds. The Content Layer’s glob loader is fast, but there’s a ceiling somewhere. The optimize: true flag on the MDX integration would be the first lever to pull.

For now, the system does exactly what it needs to: make the interesting work (building the tool) the bottleneck, not the plumbing around it.