Broken content shouldn't be able to reach production. A missing meta description, a malformed date, a required field left blank - on most sites these slip straight through, and you find out from a customer, a broken page, or a search-console warning weeks later. Each one is small; collectively they're the reason "just a content change" still causes incidents.
There's a quietly excellent tool for closing that gap, and almost nobody talks about it. Velite takes your Markdown, YAML, and JSON files, validates them against a schema at build time, transforms them, and emits a typed, importable bundle. No runtime, no database, no API - files in, typed data out. Here's why it's a great fit for dev-owned content, and the one limitation that eventually made us move on.
The problem: typed content without a CMS or a runtime
You want content as files in your repo (versioned, reviewable, fast) - but you also want guarantees. That a post always has a title under 60 characters. That every required field is present. That a typo in frontmatter is caught before deploy, not after.
A hosted CMS gives you an editor but adds a runtime dependency and a bill. Raw Markdown gives you files but no guarantees. Velite sits exactly in the middle: your files stay files, and a schema enforces the guarantees at build.
A 20-line config
You define collections with a schema - Velite uses Zod - and a bad frontmatter field fails the build instead of production:
// velite.config.ts
import { defineConfig, s } from "velite";
export default defineConfig({
collections: {
posts: {
name: "Post",
pattern: "posts/**/*.md",
schema: s.object({
title: s.string().max(60),
description: s.string().min(120).max(155),
date: s.isodate(),
slug: s.slug("posts"),
body: s.markdown(),
toc: s.toc(),
metadata: s.metadata(), // reading time, word count
}),
},
},
});
s.slug(), s.markdown(), s.toc(), and s.metadata() are built in - you get slugs, compiled Markdown, a table of contents, and reading time without writing any of it.
The payoff: a typed import that fails loudly
Consumers read content through a single typed import:
import { posts } from "#velite";
// posts is fully typed - autocomplete on every field.
export const published = posts.filter((p) => p.date <= today);
Two things fall out of this that matter to a team, not just a developer:
- Bad content fails the build. Forget a description, mistype a date, blow the title length - CI goes red. That's a whole category of "we found out from a customer" turned into a check that blocks the merge.
- Renames are safe. Change a field name and TypeScript immediately shows you every page that breaks. No guessing, no grep-and-pray.
The custom-lint trick
Because it's your build step, you can bolt on your own rules. We added an SEO lint - title ≤ 60 chars, meta description 120–155 - and ran velite build --strict in CI so a merge couldn't land with sloppy metadata. The house style stops being a thing people remember and becomes a thing the pipeline enforces.
Where it stops: the honest section
Velite is a compiler, not a CMS. That's the ceiling, and it's worth being clear-eyed about:
- No editor. A non-developer can't change content without touching the repo and its frontmatter. The moment you want a real editing UI, you add a second tool - and now you have two schemas to keep in sync.
- That two-schema drift is exactly why we left. We paired Velite with a Git-based CMS, then realised the CMS's own Reader API could replace Velite entirely and collapse everything back to one schema. (That's the other half of the story - how Keystatic's Reader API replaced our build step.)
Verdict
For content that engineers own, Velite is close to ideal: typed, validated, zero-runtime, and it makes bad content a build failure instead of a production surprise. Reassess the moment a non-developer needs to edit - that's the signal to look at a CMS, and to ask whether it can also be your content source so you don't end up maintaining two.
Choosing the right content layer looks like a small decision and quietly shapes how reliably a team can ship. Weighing exactly these trade-offs - build-time safety, who owns the content, how many moving parts you're signing up to maintain - is the kind of judgement we bring when we build custom software at Bruvora.
Frequently asked questions
- What is Velite?
- Velite is a build-time content layer. It reads your Markdown, YAML, or JSON files, validates them against a schema, transforms them, and outputs a typed bundle you import directly in your app - with no runtime, database, or content API involved.
- How is Velite different from a CMS?
- A CMS gives non-technical users an interface to edit content. Velite gives developers a typed, validated build step. It has no editor - content is edited as files in the repo - so it's ideal for dev-owned content and stops short the moment a non-developer needs to make changes.
- Does Velite add runtime overhead?
- No. All the work happens at build time and the output is static typed data, so pages just read a plain object. There's nothing extra to run or scale in production.
Keep reading
How Keystatic gives non-technical editors a real UI that commits to your Git repo - and a Reader API that replaces your content build step. One schema, and the five gotchas that cost us an afternoon.
Corporate email scanners pre-click magic links and burn the one-time token, so your B2B users hit dead links. Here's why OTP is the safer default for business software.
The modern web stack we use to ship production software fast - Next.js, Drizzle, Zod, owned UI, self-hosted fonts - and why each piece earns its place.