Extensibility
Extensibility
Stable post IDs
Every post has a stable id field computed from its slug:
id = "blog-{slug}"For example, a post with slug my-first-post always has id = "blog-my-first-post".
This ID is designed as a foreign key you can pass to any external service:
const post = await blog.getPostBySlug('my-first-post');console.log(post.id); // "blog-my-first-post"The ID is stable across renames of the markdown file, changes to the title, or
re-ordering of posts. As long as the slug is unchanged, the ID is unchanged.
Comments
Wire the stable ID into a comments backend (Giscus, Utterances, a custom API…):
import Giscus from '@giscus/react';
<Giscus repo="your-org/your-repo" repoId="R_xxx" category="Announcements" categoryId="DIC_xxx" mapping="specific" term={post.id} // ← stable foreign key reactionsEnabled="1" emitMetadata="0" theme="light"/>Using post.id instead of the URL means comments survive URL migrations.
Analytics
Pass the post ID as a custom dimension or event property:
// Google Analytics 4gtag('event', 'page_view', { page_title: post.title, blog_post_id: post.id,});
// PostHogposthog.capture('blog_post_viewed', { post_id: post.id, post_slug: post.slug, post_title: post.title, post_tags: post.tags,});Related content
Pass the post ID to an external related-content engine (Algolia, custom vector DB…):
const relatedPostIds = await myRecommendationEngine.getRelated(post.id);Or use the built-in algorithm:
const blog = createBlog({ contentDir: './content/posts', relatedPosts: { limit: 3, strategy: 'tags+category' },});
const related = await blog.getRelatedPosts('my-first-post');Strategies: 'tags' (tag overlap only), 'category' (same category only),
'tags+category' (combined, default).
Search index export
Generate a JSON search index for a client-side search library (Fuse.js, Lunr, FlexSearch…):
const index = await blog.generateSearchIndex();// [{ id, slug, title, excerpt, tags, category, author, date, readingTime }, ...]
// Write to a static file for Vite to import, or serve as an API routeimport { writeFileSync } from 'fs';writeFileSync('public/search-index.json', JSON.stringify(index));Client-side (Fuse.js example):
import Fuse from 'fuse.js';
const response = await fetch('/search-index.json');const index = await response.json();
const fuse = new Fuse(index, { keys: ['title', 'excerpt', 'tags'], threshold: 0.3,});
const results = fuse.search('nextjs tutorial').map(r => r.item);Custom schema
Add your own frontmatter fields with full type safety:
import { createBlog } from '@yamblog/core';import { z } from 'zod';
const blog = createBlog({ contentDir: './content/posts', schema: z.object({ title: z.string(), date: z.coerce.date(), author: z.string().default('Anonymous'), tags: z.array(z.string()).default([]), excerpt: z.string().optional(), draft: z.boolean().default(false), // custom fields: coverImage: z.string().optional(), featured: z.boolean().default(false), series: z.string().optional(), seriesPart: z.number().optional(), canonicalUrl: z.string().url().optional(), }),});
// Posts are now typed with your custom fields:const post = await blog.getPostBySlug('my-post');console.log(post.series); // string | undefinedconsole.log(post.seriesPart); // number | undefinedCustom sort
const blog = createBlog({ contentDir: './content/posts', sortBy: (a, b) => { // featured posts first, then newest if (a.featured && !b.featured) return -1; if (!a.featured && b.featured) return 1; return b.date.getTime() - a.date.getTime(); },});Custom slug generator
const blog = createBlog({ contentDir: './content/posts', slugify: (filename) => { // strip date prefix: "2026-01-15-hello-world.md" → "hello-world" return filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.mdx?$/, ''); },});