2026 · Novus Stream Solutions (hub)About 14 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
- 1.Overview
- 2.A post is a typed object, and the type is the spec
- 3.The whole catalog lives in version control
- 4.There are no half-published states
- 5.The same tooling builds the app and the content
- 6.Be honest about where this stops working
- 7.The payoff is that the blog cannot quietly decay
- 8.Making the object literal bearable to write in
- 9.Typed content has no lock-in
- 10.Static data builds to static pages
- 11.It scales down as gracefully as it scales up
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, A build-time validation gate: catching content errors before deploy, 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 Automating SVG hero art for a 150-post 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.
Making the object literal bearable to write in
The honest objection to typed content is that writing long prose inside a code object is less pleasant than writing it in a document, and pretending that cost away would be dishonest. But most of the friction is fixable with a little tooling, and the gap narrows quickly once you stop treating each post as a wall of escaped strings. Editor snippets that scaffold a fresh post with every required field already present turn a blank file into a fill-in-the-blanks exercise, so you are never staring at the type definition trying to remember the shape. Helper functions that build the repetitive parts — a standard call-to-action, a docs link block, the boilerplate around an image — keep the prose sections free of clutter so the actual writing reads almost like plain text in the file.
The reusable shape also pays back in consistency that a free-form editor never enforces. Because every post shares the same fields in the same order, the hundredth post is laid out exactly like the first, and an author moving between posts always knows where the excerpt, the sections, the FAQ, and the related links live. That uniformity is invisible to readers but enormous for maintenance: a script can reliably find and update any field across the whole catalog precisely because every post is built the same way. The trick is to invest a small amount in authoring ergonomics up front — snippets, helpers, a clear template — so the structure that makes the catalog safe does not also make it tedious to add to.
Typed content has no lock-in
A quiet anxiety with any content system is what happens when you outgrow it, and this is where content as code is unusually reassuring. Because the posts are plain structured data, they are trivially portable: a short script can walk the catalog and emit JSON, Markdown, or whatever shape a future system wants, with the type system guaranteeing every post has the fields the export expects. There is no proprietary database to escape, no API to reverse-engineer, no export feature to hope the vendor implemented well. The content is already in the most portable form there is — values in a file — so moving it later is a data transformation, not a migration project.
That portability is also the answer to the strongest argument against this approach, which is that the team’s needs might change. If a non-technical author joins, or the volume grows past what hand-authoring can sustain, the typed catalog can be exported into a CMS in an afternoon, and nothing about having started with code makes that harder — if anything it makes it easier, because the content is already clean, consistent, and fully structured. Choosing content as code today does not paint you into a corner tomorrow; it keeps the content in a form that every future option can read. Lock-in comes from opaque storage, and a folder of typed objects is the opposite of opaque.
Static data builds to static pages
There is a performance dividend that falls out of this model almost by accident. When the entire catalog is known at build time as static data, every page can be pre-rendered to static HTML, with the structured data, the category groupings, and the cross-links all computed once during the build rather than assembled per request against a database. There is no content query on the hot path, because there is no database in the request at all — the page a reader hits is a file that was fully assembled when the site was built. That makes the blog cheap to serve, easy to put behind a CDN, and immune to the failure mode where a content service is slow or down and takes the articles with it.
This is the same property that makes features like topic hubs and related-post sections cheap to build: they are reading data that is already in memory at build time, fully typed, so assembling them is array work rather than an integration. The content being part of the program means the program can compute anything it wants from it for free, and ship the result as static output. A CMS can be made fast with caching, but here speed is the default rather than something you bolt on, because there is no live content layer to cache in the first place. The catalog is data the build consumes and HTML the reader receives, with nothing slow in between.
It scales down as gracefully as it scales up
Most of this argument has been about a catalog of a few hundred posts, but one of the quiet virtues of content as code is how well it works at the small end too, where a CMS is most obviously overkill. A blog of ten or twenty posts maintained by a developer does not need a database, an admin interface, an editorial workflow, or a separate service to run and secure — all of which are real overhead to stand up and maintain for a handful of articles. Starting with typed objects in the repository means there is nothing to provision on day one and nothing to migrate as the catalog grows from ten posts to fifty to a few hundred; the same approach that is pleasant at small scale simply keeps working as the count climbs.
That smooth scaling is itself a reason to reach for it earlier than instinct suggests. The usual path is to start with a CMS because it feels like the grown-up choice, and then to carry its weight through the long stretch where the blog is too small to justify it. Content as code inverts that: it is light precisely when the catalog is light, and it earns its structure precisely as the catalog gets large enough for the structure to pay off, with no discontinuity in between where you have to stop and re-platform. For a developer-run project that might stay small or might grow, beginning with typed content avoids both the early overhead of a CMS and the later pain of a migration, because the thing you started with is the thing that scales.
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.