A Database-Free Content App — Parsing Markdown at Build Time in Next.js (and Doing Attribution Properly)
How the Roadmaps app turns about 94 folders of markdown into a typed, searchable site without a database: a small parser with regexes for resource lines, memoised file-system reads, title-casing rules, and the licensing hygiene of building on someone else's open-source content.
I wanted to explore a very small architecture: a content site whose database is the file system. The Roadmaps app reads about 94 roadmap folders of markdown at build time and renders them as browsable, searchable tracks. The whole data layer is a single 166-line TypeScript file. This post covers how it works, a few details that were fiddly, and the part that matters most when you build on other people's content.
The Shape of the Data
roadmaps/<slug>/content/<topic>.md
Each file has a # Title, some prose, and usually a list of links in a custom format:
If line 0 starts with # , that is the title; otherwise the title is derived from the filename slug.
A line that matches the resource regex becomes a structured resource. A "Visit the following resources…" lead-in line is dropped.
Everything else is the description. Inline tags inside prose are rewritten back into ordinary markdown links.
Doing it with two regexes rather than a full markdown AST is a deliberate trade: the format is regular and the input is trusted, so the simple approach is shorter, faster and easier to debug. If the format were free-form, I'd reach for remark instead.
Small Fiddly Things
Duplicate topics. File names can carry an @-suffix, so the loader splits on @ and keeps the first file per topic slug.
Title casing. Turning aspnet-core into "ASP.NET Core" or mlops into "MLOps" needs an acronym map plus a short list of full-slug overrides. It is unglamorous, but nothing makes a UI look careless like "Aws" and "Ai".
Stable ordering. Roadmaps and topics are sorted with localeCompare so builds are deterministic. The honest consequence is that topics appear alphabetically, which isn't how you'd want to learn them — a known limitation, and the next thing to improve with an explicit order field.
Memoisation. Each read function is wrapped in React's cache(), so the many pages generated during a build don't re-read and re-parse the same directory repeatedly.
Why No Database?
For read-only, build-time content, a database adds a service to run, a schema to migrate and a network hop to every page — and buys nothing. Files are versioned by git, reviewable in a pull request, and deployed atomically with the code. The costs are real too: no runtime editing, build time grows with content, and anything per-user needs another solution. For a site like this, it's the right side of the trade.
Attribution Is Part of the Engineering
The markdown content isn't mine. It originates from the open-source developer-roadmap project, and the repository was set up accordingly. Three decisions worth copying:
Separate licences. The repository's own licence covers only the application code. The README states that the roadmap content is not original work of this repository and links to the source project and its terms.
Don't present someone else's work as yours. The project page on this site says the same thing, up front.
Remove what you shouldn't ship. The parser filters out links pointing back to the original site, rewriting them to plain text, so the app doesn't carry the source's tracking or IDs. That was an explicit commit, made before the app was treated as finished.
A related housekeeping commit bumped Next.js to a patched release after a security advisory. A small project still needs patching.
Takeaways
For build-time, read-only content, files plus a typed parser beat a database.
Keep the parser as simple as the format allows, and write down its limits.
Do the licence and attribution work at the start; it's much harder to retrofit once a project is public.