Write like it's Notion. Publish like it's Git.
A proper editor over the repo your content already lives in. Drafts autosave privately; publishing writes one clean commit.
Kobun is free during open beta.
An editor worth writing in
Press / for headings, lists, quotes, callouts, code blocks and emoji. Drag blocks by the handle. Paste Markdown, and it stays Markdown. What Kobun commits is the file you'd have written by hand — without counting dashes in the frontmatter.
Your repo doesn't need to see the rough draft
Kobun autosaves a second after you stop typing — to Kobun, not to your repo. Nothing reaches GitHub until you publish.
Then it writes one commit. Your Git history stays a list of published posts, rather than a record of you changing your mind at eleven at night.
Your content model is a file in your repo
Add .kobun.json or .kobun.yml to your repo root. Describe your collections and singletons, and Kobun builds the right editor for each. Every entry is validated against your schema before it's committed, so a missing slug is caught in the browser, not by your build.
{
"version": 1,
"collections": {
"posts": {
"label": "Posts",
"format": "md",
"schema": {
"title": { "type": "text", "label": "Title", "required": true },
"slug": { "type": "slug", "label": "Slug", "from": "title" },
"publishedAt": { "type": "date", "label": "Published" },
"tags": {
"type": "multi_select",
"label": "Tags",
"options": [
{ "label": "Go", "value": "go" },
{ "label": "Notes", "value": "notes" }
]
},
"body": { "type": "document", "label": "Body" }
},
"features": { "publish": true, "timestamps": { "createdAt": true, "updatedAt": true } }
}
}
}---
title: Getting started with Kobun
slug: getting-started
publishedAt: 2026-07-25
cover: /images/blog/getting-started.png
author: authors/mari
draft: false
---
Kobun stores every entry as plain Markdown in your
repo. Edit it in the browser, or open it in your own
editor — it's just a file.
## Why Git-based?
Your content gets the same review, history, and
rollback story as your code.---
import { getCollection } from 'astro:content'
const posts = (await getCollection('posts'))
.filter((post) => !post.data.draft)
.sort(
(a, b) =>
b.data.publishedAt.valueOf() -
a.data.publishedAt.valueOf(),
)
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.data.slug}`}>{post.data.title}</a>
</li>
))}
</ul>import fs from 'node:fs/promises'
import path from 'node:path'
import matter from 'gray-matter'
const dir = path.join(process.cwd(), 'content/posts')
export default async function BlogIndex() {
const files = await fs.readdir(dir)
const posts = (
await Promise.all(
files.map(async (file) => {
const raw = await fs.readFile(path.join(dir, file), 'utf8')
return matter(raw).data
}),
)
)
.filter((post) => !post.draft)
.sort((a, b) => +new Date(b.publishedAt) - +new Date(a.publishedAt))
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
)
}import matter from 'gray-matter'
import type { Route } from './+types/blog'
const files = import.meta.glob('../content/posts/*.md', {
eager: true,
query: '?raw',
import: 'default',
}) as Record<string, string>
export function loader() {
return Object.values(files)
.map((raw) => matter(raw).data)
.filter((post) => !post.draft)
.sort((a, b) => +new Date(b.publishedAt) - +new Date(a.publishedAt))
}
export default function Blog({ loaderData }: Route.ComponentProps) {
return (
<ul>
{loaderData.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
)
}import matter from 'gray-matter'
import { For } from 'solid-js'
import { A } from '@solidjs/router'
const files = import.meta.glob('~/content/posts/*.md', {
eager: true,
query: '?raw',
import: 'default',
}) as Record<string, string>
const posts = Object.values(files)
.map((raw) => matter(raw).data)
.filter((post) => !post.draft)
.sort((a, b) => +new Date(b.publishedAt) - +new Date(a.publishedAt))
export default function Blog() {
return (
<ul>
<For each={posts}>
{(post) => (
<li>
<A href={`/blog/${post.slug}`}>{post.title}</A>
</li>
)}
</For>
</ul>
)
}<script lang="ts">
import matter from 'gray-matter'
const files = import.meta.glob('$lib/content/posts/*.md', {
eager: true,
query: '?raw',
import: 'default',
}) as Record<string, string>
const posts = Object.values(files)
.map((raw) => matter(raw).data)
.filter((post) => !post.draft)
.sort(
(a, b) =>
+new Date(b.publishedAt) - +new Date(a.publishedAt),
)
</script>
<ul>
{#each posts as post (post.slug)}
<li>
<a href="/blog/{post.slug}">{post.title}</a>
</li>
{/each}
</ul>Kobun fits around what you already have
Any generator
Astro, Hugo, Eleventy, Jekyll, Next, or something you wrote yourself. Kobun never touches your build.
Any host
Vercel, Netlify, Cloudflare, GitHub Pages. Kobun has no opinion about where your site is served.
No dependencies
No package in your project, no admin route, no extra dependency. A GitHub App and a config file.
Content you keep
Plain Markdown, JSON and YAML, in your repository. Close your Kobun account tomorrow and every post is still exactly where it was.
Let me run it, or run it yourself
Same Kobun either way. The only difference is who operates it.
Hosted
Sign in with GitHub and pick a repository. I keep it running.
- Free during early access
- 3 repositories free when pricing lands
- Updates & infrastructure handled
- Start writing in minutes
Self-hosted
Run Kobun on your own Cloudflare account. Your data, your bill, your uptime.
- Cloudflare Workers and D1
- Your own Github App and secrets
- No limits beyond Cloudflare's
- Source-available under FSL-1.1-MIT
FSL-1.1-MIT: free to use, modify and self-host. The one restriction is selling a competing hosted Kobun. Every version becomes MIT two years after it ships.
Where Kobun actually is
Kobun is at version 0.3, and I publish my own sites with it. Sign in with GitHub, connect a repo that has a config file, and you can be writing within a few minutes.
What isn't there yet:
- Browsing version history inside the dashboard
- Uploading images from the editor
- Automatic schema detection from an existing repo
- More field types
- Published content has a full Git history — you'd read it on GitHub for now.
You'll find bugs. Tell me and I'll fix them.
Start writing
Connect a repository, add a config file, and see whether the editor holds up. It takes about five minutes and costs nothing.