2026 · Novus Stream Solutions (hub)About 8 min readNovus Stream Solutions
Content as code: running a 250-post blog without a CMS
The default assumption is that a blog of any size needs a content-management system. For a catalog of a couple of hundred posts maintained by the same people who write the code, storing every post as a typed object in the repository is simpler, safer, and faster — and the type checker becomes your editor.
Contents
Overview
When a blog reaches a couple of hundred posts, the reflex is to reach for a content-management system. That reflex is usually correct — for a newsroom with a dozen writers, an editorial calendar, and non-technical authors, a CMS is exactly the right tool. But the reflex is worth questioning when the blog is maintained by the same small group of people who write the application code, deployed from the same repository, and never touched by anyone who would balk at editing a file. Under those conditions a CMS can be more overhead than help: another service to run, another database to back up, another place where the content and the code can drift out of sync. The product blog took the other road and stores every post — all roughly two hundred and fifty of them — as a typed object in the codebase. This is the case for that choice, and an honest map of where it stops working.
The shorthand is “content as code”: a blog post is not a row in a database edited through a web form, it is a structured value in a source file, checked by the same compiler that checks the rest of the app and shipped through the same build. That sounds austere, and for the wrong team it would be. But for a developer-maintained catalog it turns out to convert a pile of soft, easy-to-get-wrong content problems into hard, easy-to-catch code problems, which is a very good trade. The reason it works is that the things that go wrong with a large blog — a missing field, a broken link, a post that is half-finished — become things a type checker and a build step can refuse to let through.
A post is a typed object, and the type is the spec
Every post conforms to a single type that names every field it can have: the title, the date, the excerpt, the sections, the optional FAQ, the related posts, the metadata overrides. That type is not documentation that drifts; it is enforced. If a post is missing its title or its date, the project does not build. If someone adds a section with a heading but forgets the paragraphs, the compiler says so by name and points at the line. The schema stops being a convention people are supposed to remember and becomes a contract the machine checks on every change, which is the difference between a style guide and a guardrail.
This is the quiet superpower of the approach. In a CMS, a content model is enforced softly — a required field might be marked required, but a malformed entry, an orphaned reference, or a field used inconsistently across years of posts tends to slip through and surface later as a rendering bug. With typed content, the entire catalog is re-validated against the schema every time anything changes, so an inconsistency introduced two hundred posts ago and a typo introduced this morning are caught by the same mechanism. The post shape and the editor are the same thing, and that thing never sleeps.
The whole catalog lives in version control
Because the posts are source files, the entire history of the blog is in git, with everything that brings. Every edit is a diff you can read, attribute, and revert. A change to forty posts at once — renaming a product, fixing a recurring phrasing, adding a field across the catalog — is a single reviewable commit rather than forty manual edits in a web interface with no record of who changed what or why. Publishing is a merge; rolling back a bad change is a revert. The content has the same disaster-recovery story as the code, which is to say a real one, instead of depending on whatever backup discipline a separate CMS database happens to have.
This also means content review works exactly like code review. A new post arrives as a change set, gets read in context alongside the diff, and merges when it is right. There is no separate publishing workflow to build and secure, no preview environment to keep in sync with production, because the preview is just the branch. For a team that already lives in pull requests, folding content into that flow removes an entire parallel process rather than adding one. The same habits that keep the code honest — review, history, atomic changes — now keep the content honest too.
There are no half-published states
A subtle but real benefit is that content as code has no concept of a post that is live but broken. In a CMS, it is entirely possible to publish an entry that references an image that was never uploaded, links to a page that does not exist, or relies on a field an editor left blank — the system happily serves it and a reader finds the breakage before you do. With typed content built through a validating step, a post that references a missing illustration or an internal link that resolves nowhere is a build failure, not a live page. The post either is correct and ships, or is incorrect and blocks the deploy. There is no third state where it is wrong and public.
That property compounds across a large catalog. The fear with two hundred and fifty posts is the long tail of small rot — a renamed route that silently orphaned ten old links, an image path that broke in a refactor — and the thing about that rot is that nobody goes looking for it. Making the build the gatekeeper means the catalog is re-checked in its entirety on every change, so a link that breaks today because of an unrelated change is caught today, by the person who caused it, rather than discovered months later by a reader hitting a dead page. The detail of how those checks are built is its own article, /product-blog/a-build-time-validation-gate-for-content, but the principle is that correctness is enforced at build time rather than hoped for at run time.
The same tooling builds the app and the content
When content is code, every tool you already have for code applies to it for free. Find-and-replace across the catalog is just editing files. Generating the sitemap, the RSS-style index, the category pages, and the JSON-LD for every post is a matter of mapping over an array of typed objects, with the compiler guaranteeing each one has the fields those generators expect. Even the illustrations are generated from code in the same repository, described in /product-blog/automating-svg-hero-art-for-a-blog, so a post and its art are versioned together and reviewed together. There is no integration to maintain between a content service and the site, because there is no boundary to integrate across.
This is also what makes the catalog programmable in ways a CMS makes awkward. Want to surface every post in a category, or compute related posts, or assemble a topic hub from the existing articles? That is array manipulation over typed data, not an export-and-transform dance against a content API. The blog becomes a data structure the rest of the site can query directly, which is why features like category landing pages and automatic cross-linking are cheap to add — they are reading a value that is already in memory, fully typed, at build time. The content is not behind a wall; it is part of the program.
Be honest about where this stops working
This approach is not a universal recommendation, and pretending otherwise would undercut the whole argument. It works because of a specific set of conditions: the authors are comfortable editing code, the team is small enough that everyone touching content can run the build, and the publishing cadence does not require a non-technical person to ship a post at three in the afternoon without a developer nearby. Break any of those and the calculus flips. A marketing hire who needs to publish independently, a freelance writer who should never see the repository, a volume of posts that makes typed objects unwieldy to author — each is a real reason a CMS earns its keep, and ignoring that is how engineers talk themselves into building a worse CMS by hand.
There is also a middle ground worth naming, because the choice is not binary. File-based content with Markdown or MDX keeps most of the version-control and review benefits while being far friendlier to write long prose in than a TypeScript object literal, and it is the right answer for many teams who want files over a database but do not want to escape every apostrophe in a string. The typed-object approach makes the most sense when the content is highly structured — lots of fields, FAQs, related posts, metadata — and tightly coupled to features the site computes from it, which is exactly the situation here. The honest summary is that content as code is a fit for developer-run, structure-heavy, moderate-volume blogs, and the further you are from that profile, the more a CMS or an MDX setup will serve you better.
The payoff is that the blog cannot quietly decay
The deepest reason to run content this way is not any single convenience; it is that a large catalog maintained as typed, validated, version-controlled code is structurally resistant to the slow decay that afflicts big blogs. The usual story of a two-hundred-post blog is entropy: dead links accumulate, the schema drifts as conventions change, old posts reference products that were renamed, and nobody has the appetite to audit it all. When the catalog is re-validated on every build, that entropy has nowhere to hide — a change that would break old posts breaks the build instead, so the cost of keeping the catalog correct is paid continuously in small amounts rather than deferred into an audit nobody schedules.
That is the trade at the heart of content as code. You give up the convenience of a friendly editing interface and the ability to hand publishing to non-developers, and in exchange you get a blog where correctness is enforced rather than hoped for, where the whole history is legible, and where the content is just data the program can use. For a developer-maintained product blog it is a trade worth making, and the proof is mundane in the best way: a catalog this size stays coherent without anyone having to remember to keep it that way. If your blog fits the profile, the type checker makes a surprisingly good editor.
Frequently asked questions
Quick answers to common questions about this topic.
Why not just use a CMS for a blog this size?
For many teams you should. A CMS earns its keep when non-technical people need to publish independently, when you have a dozen authors, or when the volume makes hand-authoring impractical. Content as code makes sense in the opposite situation: a small, developer-run team where everyone touching the content can run the build, and where the content is structured enough that a type checker enforcing the schema is a genuine asset.
Is storing posts as TypeScript objects not painful to write prose in?
It is the main downside — escaping strings and living inside an object literal is less pleasant than writing Markdown. That is exactly why MDX or Markdown files are a popular middle ground: they keep version control and review while being friendlier for long prose. The typed-object approach wins when content is highly structured (many fields, FAQs, related posts, metadata) and tightly coupled to features computed from it.
What stops a broken post from going live?
The build. Because every post is checked against a single type and run through a validating build step, a post missing a required field, referencing a missing image, or linking to a route that does not exist becomes a build failure rather than a live, broken page. There is no state where a post is both wrong and published — it either passes the checks and ships or fails them and blocks the deploy.
How does content as code help with SEO and structured data?
Because every post is a typed object with known fields, generating the sitemap, category pages, and JSON-LD (BlogPosting, FAQPage, and so on) is just mapping over the catalog, with the compiler guaranteeing each post has what those generators need. Correct structured data for every article falls out of the data model rather than being maintained by hand per post.
How do you handle a large change across many posts?
As a single reviewable commit. Renaming a product across forty posts, fixing a recurring phrase, or adding a new field to the whole catalog is a find-and-replace or a scripted edit, reviewed as one diff and merged at once — with full git history of who changed what and why, and a one-command revert if it was wrong. That is far safer than editing forty entries by hand in a web interface.