Skip to content

Getting Started

Yamblog is a file-based, type-safe, framework-agnostic markdown blog engine. Write posts as .md files. Get a fully-featured blog. No database, no CMS.

Install

Pick the adapter for your framework:

Terminal window
# Next.js
npm install @yamblog/core @yamblog/next
# Astro
npm install @yamblog/core @yamblog/astro
# React (Vite / CRA)
npm install @yamblog/core @yamblog/react

If you want an LLM to do the integration for you, use the dedicated Prompt for LLMs guide instead of improvising a one-line install request.

Create your first post

Posts live in a directory of your choice — content/posts/ is the convention:

content/
posts/
hello-world.md

Every post needs a frontmatter block:

---
title: "Hello, World!"
date: "2026-01-15"
author: "Your Name"
tags: ["intro"]
excerpt: "My very first post."
draft: false
---
Welcome to my blog! This is the body of the post in **markdown**.

Required fields: title, date. Optional but recommended: author, tags, excerpt, draft.

The slug is derived from the filename and sanitized into a URL-safe form — hello-world.md becomes /blog/hello-world, and My Post.md becomes /blog/my-post. Do not put slug in frontmatter; it is a system field.

Create a blog instance

lib/blog.ts
import { createBlog } from '@yamblog/core';
export const blog = createBlog({
contentDir: './content/posts',
});

createBlog returns a Blog object. All methods return Promises and are safe to call in parallel — results are cached after the first load. In development (NODE_ENV=development) the cache is refreshed whenever a content file changes, so edits show up without restarting the dev server.

Useful options:

export const blog = createBlog({
contentDir: './content/posts',
siteUrl: 'https://example.com', // base for RSS / sitemap / JSON-LD links
basePath: '/blog', // URL prefix where posts are served (default '/blog', '' for site root)
includeDrafts: false, // set true to preview posts marked draft: true in queries
});

Query your posts

const posts = await blog.getPosts(); // all published posts
const post = await blog.getPostBySlug('hello-world'); // throws PostNotFoundError if missing
const maybe = await blog.findPostBySlug('hello-world'); // null if missing
const featured = await blog.getFeaturedPosts();
const tags = await blog.getTags();
const results = await blog.search('hello');

Every method also has a synchronous twin (getPostsSync(), generateRssSync(...), …) for non-async contexts — Pages Router getStaticProps, module-scope constants, or standalone build scripts.

Missing posts are catchable without string-matching:

import { PostNotFoundError } from '@yamblog/core';
try {
await blog.getPostBySlug(slug);
} catch (err) {
if (err instanceof PostNotFoundError) notFound(); // err.slug available
else throw err;
}

Extend the schema

Use Zod to add custom frontmatter fields — the schema’s inferred type flows to every method that returns posts, so custom fields are typed with no casts:

import { createBlog, defaultSchema } from '@yamblog/core';
import { z } from 'zod';
const blog = createBlog({
contentDir: './content/posts',
schema: defaultSchema.extend({
videoUrl: z.string().url().optional(),
}),
});
const post = await blog.getPostBySlug('hello-world');
post.videoUrl; // string | undefined — typed automatically

Next steps