Skip to content

Blog ​

Publish a personal blog managed entirely by your AI assistant. Write in Markdown, customize themes, and share posts with peers — all from a conversation.

What is this? ​

Crow includes a built-in blogging platform. You write posts by talking to your AI, and Crow handles rendering, publishing, RSS feeds, and theming. Posts are served as public HTML pages from your gateway.

Why would I want this? ​

  • Write by talking — Describe what you want to write, and Crow drafts and publishes it
  • Own your content — Posts live on your own server, not a third-party platform
  • Built-in RSS — Readers can subscribe via RSS or Atom feeds without any extra setup
  • Share with peers — Send posts directly to connected Crow users through P2P sharing
  • Export anytime — Move your content to Hugo, Jekyll, or any static site generator

Setup & Public Access ​

Starting the blog server ​

Blog tools are available through two transport options:

  • Stdio (MCP) — Run npm run blog-server to start the blog MCP server directly. This is what .mcp.json uses when Claude Code or another MCP client connects.
  • Gateway (HTTP) — Run npm run gateway to start the HTTP gateway, which hosts all MCP servers including the blog. The gateway also serves the public blog pages.

The blog MCP tools (create, edit, publish, etc.) work in both modes. Public web access to your blog requires the gateway.

Public blog routes ​

When the gateway is running, the blog is served at these public routes:

RouteDescription
/blogBlog homepage listing all published public posts
/blog/:slugIndividual post page (e.g., /blog/notes-on-local-first-software)
/blog/tag/:tagPosts filtered by tag
/blog/feed.xmlRSS 2.0 feed
/blog/feed.atomAtom 1.0 feed
/blog/sitemap.xmlXML sitemap for search engine indexing

RSS autodiscovery <link> headers are included automatically on all blog pages, so feed readers can find your feeds from any blog URL.

Gateway configuration ​

Set CROW_GATEWAY_URL in your .env file to your public-facing URL:

bash
CROW_GATEWAY_URL=https://blog.example.com

This controls how URLs are generated in RSS/Atom feeds, Open Graph meta tags, and the sitemap. Without it, all URLs default to http://localhost:3001, which will not work for external visitors or social media previews.

Making it publicly accessible ​

The gateway listens on localhost:3001 by default. To make your blog reachable from the internet:

  • Tailscale Funnel — Easiest option for Tailscale users. No port forwarding or domain registration needed. See details below.
  • Caddy or nginx reverse proxy — Use a reverse proxy with automatic Let's Encrypt certificates for a custom domain. See Caddy example below.
  • Direct port forwarding — Forward port 3001 on your router. Not recommended: exposes the full gateway (Crow's Nest, MCP endpoints) without TLS.

For a full deployment comparison, see the Making Your Blog Public section further down.

Writing a Post ​

Ask Crow to create a post:

"Write a blog post about my garden project this weekend"

"Create a draft post titled 'Notes on Local-First Software'"

Crow creates the post in Markdown, generates a URL-friendly slug, and saves it as a draft.

Post Fields ​

Each post has:

  • Title — Displayed as the page heading and in feeds
  • Slug — The URL path (e.g., /blog/notes-on-local-first-software)
  • Content — Markdown body, rendered to HTML on publish
  • Tags — Optional categories for organization
  • Status — draft or published
  • Visibility — Controls who can see the post:
    • private — Only you (default)
    • public — Anyone with the URL
    • peers — Only connected Crow peers

Publishing ​

When you're happy with a draft:

"Publish my post about local-first software"

The post becomes publicly accessible at http://your-server:3001/blog/notes-on-local-first-software.

To unpublish:

"Unpublish the garden project post"

Editing Posts ​

"Update my garden post — add a section about the raised beds"

"Change the title of my latest post to 'Weekend Garden Notes'"

Crow modifies the post in place. Published posts update immediately.

Listing and Searching ​

"Show me all my blog posts"

"Find posts tagged 'research'"

"Search my blog for 'neural networks'"

Blog posts are indexed with FTS5 full-text search, so keyword searches are fast even with hundreds of posts.

Themes ​

The blog uses the Dark Editorial theme by default — a clean, reading-focused design with dark and light mode support. The blog's color palette follows the Crow brand identity: indigo accents, deep blue-black backgrounds in dark mode, and warm stone tones in light mode. This ensures visual consistency between your blog and the Crow's Nest.

The theme controls:

  • Typography and layout
  • Code block syntax highlighting
  • Header and footer styling
  • Open Graph meta tags for social sharing previews

RSS and Atom Feeds ​

Feeds are generated automatically:

  • RSS 2.0: http://your-server:3001/blog/feed.xml
  • Atom: http://your-server:3001/blog/feed.atom

Feeds include the 20 most recent published posts with full content.

Exporting ​

Move your content to a static site generator:

"Export my blog posts for Hugo"

"Export all posts as Jekyll-compatible Markdown"

Crow generates Markdown files with the appropriate frontmatter format for your target platform.

Sharing Posts with Peers ​

If you have connected peers (see the Sharing guide), you can send posts directly:

"Share my latest blog post with Alice"

The recipient gets the full post content in their Crow inbox.

Blog Configuration ​

Set blog metadata in your .env:

bash
CROW_BLOG_TITLE=My Blog
CROW_BLOG_DESCRIPTION=Thoughts on technology and gardening
CROW_BLOG_AUTHOR=Your Name

These values appear in the RSS feed and page headers.

Collections Page ​

The blog can show an optional landing page at /blog/research that groups related posts: case studies from one project, and posts carrying a tag. It is off by default. With no config file, /blog/research returns 404, the blog navigation shows no link to it, and posts render without chapter navigation.

To turn it on, create ~/.crow/blog/collections.json (more precisely <CROW_HOME>/blog/collections.json; set CROW_BLOG_COLLECTIONS_CONFIG to use a different path). The gateway re-reads the file when it changes, so no restart is needed. Deleting it turns the page off again.

json
{
  "enabled": true,
  "navTitle": "Notes",
  "pageTitle": "Example collection",
  "intro": "Long-form notes from the field.",
  "lang": "en",
  "featured": {
    "title": "Start here",
    "blurbHtml": "An <em>overview</em> of the series, with a <a href=\"https://example.com/\">reference</a>."
  },
  "sections": [
    { "kind": "case-studies", "heading": "Entries", "projectId": 2 },
    {
      "kind": "tag",
      "heading": "Field notes",
      "tag": "field-notes",
      "blurb": "Short reads.",
      "docLinks": {
        "base": "https://docs.example.com/notes/",
        "rules": [{ "slugContains": "summary", "file": "summary.pdf" }],
        "defaultFile": "notes.pdf"
      }
    }
  ],
  "chapterNav": { "tag": "case-study", "projectId": 2, "indexLabel": "All entries" }
}
  • case-studies sections list published, public posts linked from a project's case studies (Data Dashboard extension), in their display order. Without that extension the section shows its empty text.
  • tag sections list published, public posts with the tag. They are hidden when empty unless hideIfEmpty is false. docLinks adds one link per post: the file of the first rule whose slugContains appears in the slug, otherwise defaultFile, appended to base. base must be an http(s) URL or a /blog/ path. Crow serves no files under /blog/ for this, so a /blog/ base only works if something else (your reverse proxy, another route) serves that path; otherwise the links 404.
  • chapterNav adds previous/next links between the project's case-study posts that carry its tag.
  • lang (en or es) sets the page language and its built-in labels.
  • featured.blurbHtml accepts a small amount of HTML: <em>, <strong> and <a href> with an absolute http(s) URL. Everything else is removed. All other strings are shown as plain text.

The schema is strict: an unknown key, a bad value or more than eight sections disables the page and logs one warning that names the file and the problem (never the values). Nothing in Crow writes this file; you edit it yourself.

Figures that the Data Dashboard renders for blog posts are stored in the object-storage bucket named by CROW_BLOG_FIGURES_BUCKET (default blog-figures). Set it in the gateway's environment (its .env file or a systemd drop-in for the gateway unit), not in the Data Dashboard extension's settings: the gateway serves /blog/figures/* from its own environment, and the extension inherits that environment, so both always use the same bucket.

Making Your Blog Public ​

Your blog has no web presence without the gateway running. How your blog becomes accessible depends on your deployment:

DeploymentBlog accessible?How
Desktop (stdio)NoGateway not running — no web blog
Self-hosted (Pi/server)LAN only by defaultAvailable at http://<server-ip>:3001/blog on your local network
Cloud (Render/Oracle)Yes — public internetBlog at https://your-crow-server/blog

For self-hosted setups, see the sections below to make your blog accessible from the internet.

Tailscale Funnel exposes your gateway to the public internet through Tailscale's infrastructure — no port forwarding, no dynamic DNS, no domain registration needed.

bash
# Enable Funnel in your Tailscale admin console first:
# https://login.tailscale.com/admin/dns → Enable Funnel

# Then expose your gateway
tailscale funnel --bg --https=443 http://localhost:3001

Your blog is now publicly accessible at https://<hostname>.your-tailnet.ts.net/blog.

The Crow's Nest remains private — requests from public IPs get a 403 response because they don't fall within the allowed network ranges. Only the blog (and other unauthenticated routes like /health and /setup) are effectively visible to the public.

Custom Domain with Caddy ​

If you want a custom domain that only serves your blog (not the full gateway), you can configure Caddy as a reverse proxy with path restrictions.

WARNING

This Caddyfile replaces the default full-gateway proxy. If you're using Caddy to serve the Crow's Nest over Tailscale, you'll need separate Caddy configurations or a combined Caddyfile.

yourdomain.com {
    # Only proxy blog and health routes
    handle /blog* {
        reverse_proxy localhost:3001
    }
    handle /health {
        reverse_proxy localhost:3001
    }

    # Block everything else
    handle {
        respond "Not Found" 404
    }
}

Caddy automatically provisions Let's Encrypt certificates for your domain.

Setting CROW_GATEWAY_URL ​

For RSS feed links, Open Graph meta tags, sitemap URLs, and social media previews to work correctly, set the public URL of your gateway:

bash
# In your .env file
CROW_GATEWAY_URL=https://yourdomain.com

Without this, links in feeds and social previews will point to http://localhost:3001, which won't work for external visitors.

Under the Hood ​

For how posts, feeds, and public routes work internally, see the Blog Server architecture.

Released under the MIT License.