bruvora logoBruvora
Engineering

Catch Broken Content Before It Ships: The Case for Velite

Velite validates your Markdown and YAML against a schema at build time and emits typed data - no CMS, no database, no runtime. Here's where it shines for teams, and the one limit that made us move on.

Author
Jatin Bumbra
Founder
Date
July 10, 2026
4 min read
Share

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.
Sources

Keep reading

Need this built?

This is the kind of work we do for clients.

See What Your Systems Are Costing You.

Tell us where things get stuck. We’ll tell you what it’s costing and what we’d do about it.