Blog Server
The blog server (servers/blog/) provides a publishing platform through MCP tools and public HTTP routes. Posts are written in Markdown, rendered to HTML, and served with RSS/Atom feeds.
Architecture
┌──────────────────────────────────────┐
│ MCP Tools Layer │
│ crow_create_post crow_list_posts │
│ crow_edit_post crow_get_post │
│ crow_publish_post crow_unpublish │
│ crow_delete_post crow_share_post │
│ crow_export_blog crow_blog_stats │
│ crow_blog_settings │
│ crow_blog_customize_theme │
├──────────────────────────────────────┤
│ Public HTTP Routes │
│ GET /blog (post list) │
│ GET /blog/:slug (single) │
│ GET /blog/feed.xml (RSS 2.0) │
│ GET /blog/feed.atom (Atom) │
├──────────────────────────────────────┤
│ renderer.js │ rss.js │
│ Markdown → HTML │ Feed gen │
├──────────────────────────────────────┤
│ SQLite (blog_posts) │
│ FTS5 index (blog_posts_fts) │
└──────────────────────────────────────┘Factory Pattern
// servers/blog/server.js
export function createBlogServer(dbPath) {
const server = new McpServer({ name: "crow-blog", version: "1.0.0" });
// ... tool registrations
return server;
}server.js— Factory function and tool definitionsindex.js— Stdio transport bindingrenderer.js— Markdown rendering and HTML sanitizationrss.js— RSS 2.0 and Atom feed generation
renderer.js
Converts Markdown post content to safe HTML:
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
export function renderPost(markdown) {
const rawHtml = marked.parse(markdown);
return sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'h3']),
allowedAttributes: { ...sanitizeHtml.defaults.allowedAttributes, img: ['src', 'alt'] }
});
}The sanitizer strips scripts, iframes, and event handlers while preserving standard formatting, images, and code blocks.
rss.js
Generates RSS 2.0 and Atom feeds from published posts:
export function generateRssFeed(posts, blogConfig) { }
export function generateAtomFeed(posts, blogConfig) { }Feeds include the 20 most recent published posts with full rendered HTML content. Blog metadata (title, description, author) comes from environment variables.
Database
blog_posts table
CREATE TABLE blog_posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
content TEXT NOT NULL, -- Raw Markdown
excerpt TEXT, -- Short excerpt (auto-generated or manual)
author TEXT, -- Author name
status TEXT DEFAULT 'draft', -- 'draft', 'published', or 'archived'
visibility TEXT DEFAULT 'private', -- 'private', 'public', or 'peers'
cover_image_key TEXT, -- S3 key for cover image
tags TEXT, -- Comma-separated tags
nostr_event_id TEXT, -- For P2P sharing
published_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);FTS5 index
CREATE VIRTUAL TABLE blog_posts_fts USING fts5(
title, content, excerpt, tags,
content=blog_posts,
content_rowid=id
);Triggers keep the FTS index in sync on insert, update, and delete — the same pattern used by the memory and research servers.
Slug Generation
Slugs are generated from titles:
- Convert to lowercase
- Replace spaces and special characters with hyphens
- Remove consecutive hyphens
- Trim to 80 characters
- If a slug already exists, append a numeric suffix (
-2,-3, etc.)
Public Routes
These routes are served by the gateway without authentication:
GET /blog
Renders an HTML page listing all published posts, newest first. Uses the Dark Editorial theme template.
GET /blog/:slug
Renders a single post as a full HTML page with:
- Post title and publication date
- Rendered Markdown content
- Open Graph meta tags for social sharing
- Structured data (JSON-LD) for search engines
- Navigation links to previous/next posts
GET /blog/tag/:tag
Posts filtered by a specific tag.
GET /blog/feed.xml
RSS 2.0 feed of published public posts.
GET /blog/feed.atom
Atom feed of published public posts.
Dark Editorial Theme
The blog's visual design uses server-side HTML templates (no client-side JavaScript framework). Key characteristics:
- Serif typography for body text, sans-serif for headings
- Generous whitespace and readable line lengths
- Dark mode by default, light mode via CSS
prefers-color-scheme - Syntax highlighting for code blocks
- Responsive layout for mobile and desktop
Templates are embedded in the server code — no external template files or build step.
Open Graph Meta
Every published post includes Open Graph tags:
<meta property="og:title" content="Post Title" />
<meta property="og:description" content="First 200 characters..." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://your-server/blog/post-slug" />This ensures proper previews when posts are shared on social media or messaging apps.
Export
The export tool generates Markdown files with platform-specific frontmatter:
Hugo format:
---
title: "Post Title"
date: 2026-03-01T12:00:00Z
tags: ["tag1", "tag2"]
draft: false
---Jekyll format:
---
layout: post
title: "Post Title"
date: 2026-03-01
categories: [tag1, tag2]
---