Type-safe content pipeline
Markdown in.
TypeScript out.
Point qino at your content folder and hand it a schema. You - and your agents - get typed, validated content. Nothing to deploy!
What developers say
I thought using Markdown for content would be easy. Then I saw how much wiring and formatting it takes.... qino just handles all of that, amazing tool!

Fullstack Engineer, Rainbow
Why I built qino, and why now?
At my last company, we ran several websites on Markdown content: blogs, event listings, and landing pages. Markdown is great until you need non-developers to publish updates. Then we developers become the bottleneck.
We were looking for CMS tools that plug into our existing sites, instead of forcing our code into their framework.
We first tried Decap CMS. It's free and open-source, but we had to maintain a config.yml that grew with every content type, and still fetch and validate the Markdown ourselves, with no type safety.
Then we gave TinaCMS a try. It handled fetching and typing, but made us query our own files through GraphQL, and its git workflow confused developers and editors alike.
Two tools, same result: more time spent on the CMS than on the content. I just wanted it to be simpler.
I still believed in Markdown as a content source, and the timing made the case even stronger. AI agents now work alongside you in the codebase, and they're only as useful as the content they can read. A hosted CMS keeps that content on someone else's servers, behind APIs that can rate limit you, go down, or charge for access.
The best way to give agents your content is to keep it local, in your repo, right next to your code. Owning your content layer has never mattered more.
So I built qino, and it comes with:
- Type-safe content. Define your content with Zod schemas. Types hold from end to end.
- No config file to maintain. qino generates it from your schemas and settings.
- No framework lock-in. qino plugs into the site you already have.
- Any content shape. Collections, standalone pages, or full documentation sites.
- A cloud editor, soon. Non-developers edit content without touching code.
- A CLI for agents.
qino checkvalidates every edit against your schemas.
You can use @qino/cms as a standalone content pipeline for your files, or pair it with the cloud UI for a full CMS experience once it ships.
Give qino a try! Read the docs or let your agent set it up.
Creator of qino
qino is dead simple to set up
qino works with what you already have: Markdown files in your repo, a TypeScript config, and your framework of choice.
01 — Markdown on disk
.
├── content
│ ├── posts
│ │ ├── hello-world.md
│ │ └── typed-content.md
│ └── authors
│ ├── camille-laurent.json
│ └── jonas-weiss.json
├── qino
│ ├── collections
│ │ ├── posts.ts
│ │ └── authors.ts
│ └── index.ts
└── src
└── ...02 — qino
const postsCollection = qino.defineCollection({
directory: "/posts",
extension: ".md",
schema: PostSchema,
relations: {
author: authorsCollection,
},
views: (view) => ({
default: view({ resolveRelations: 1 }),
}),
})03 — In your app
await postsCollection.getEntries();
// [
// {
// title: "Hello world",
// author: { name: "Camille Laurent" },
// _meta: { slug: "hello-world" },
// },
// {
// title: "Typed content",
// author: { name: "Jonas Weiss" },
// _meta: { slug: "typed-content" },
// },
// ]Supported file types
Reads the files
you already have
Markdown
MDX
JSON
Three ways to model content
- Collection
Unordered list of of entries
A flat folder of similarly shaped entries such as
posts/*.md. Great for blog posts, events, team members, etc. Views filter, sort and paginate.- Read more about collections
- Tree
Hierarchy and order, first-class
Nested folders with anchor files and
_order.json. Great for docs, guides or a knowlegde base. Build a sidebar without validating a single body.- Read more about trees
- Item
One file with its own role
A single file, such as
pages/home.md, with a unique schema. Great for pages or navigation.- Read more about items
Relations
qino makes your content a relational database.
Declare a relation and a frontmatter string becomes a typed foreign key. Any primitive can point at any other; the view decides how many hops resolve, up to 6.
const postCollection = qino.defineCollection({
directory: "/posts",
extension: ".md",
schema: z.object({
title: z.string(),
author: z.string(),
categories: z.array(z.string()),
tags: z.array(z.string()),
}),
relations: {
author: authorCollection,
"categories[*]": categoryCollection,
"tags[*]": tagCollection,
},
views: (view) => ({
default: view({ resolveRelations: 1 }),
}),
});- posts.author
- authors
- posts.categories[*]
- categories
- posts.tags[*]
- tags
A broken relation should fail the build, not the page.
When a view resolves a relation, qino checks that the linked entry exists. If it doesn't, the build fails, so broken links never ship.
One command between your content and a broken build.
The CLI runs before your framework does, so content errors surface in the terminal instead of at request time.
- qino build
Generate types
Checks every definition, validates every content file against its schema, and writes
qino/_generated/types.d.ts. Add it to your prebuild script inpackage.jsonand every deploy is gated on valid content.- qino check
Validate content only
Fast feedback in CI or on save. Relation fields are treated as ordinary strings and are not followed.
- qino lint
Validate definitions only
Checks that content and media folders exist, that no two primitives own overlapping paths, and that relations target the same instance.
Let your agent set it up.
Install the qino skill, then ask your coding agent to set up qino. It models the content you already have into collections, trees, and items, wires qino build into your build, and keeps going until it passes.