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

Stop Renting a Compromise.

Tell us what your tools can’t do. Describe the workaround you’re tired of, and we’ll tell you what it’d take to replace it - no pitch, no obligation.