# StaticBlog
A reusable static blog engine for Elixir. Generates a complete website from markdown files with syntax highlighting, RSS feed, sitemap, SEO structured data, and a Daring Fireball-inspired default template. Includes a Micropub/XML-RPC editing server for MarsEdit integration and a publisher that syncs the built site to Cloudflare R2.
Each blog is its own Mix project that depends on `:static_blog` as a library. Content lives in `priv/posts/`, templates are swappable via a behaviour, and all configuration is under the `:static_blog` application key.
## Quick start
Generate a new Mix project and add the dependency:
```elixir
# mix.exs
def deps do
[
{:static_blog, "~> 0.1"},
{:nimble_publisher, "~> 1.1"},
{:makeup_elixir, "~> 0.16"},
{:makeup_erlang, "~> 0.1"}
]
end
```
Configure your site in `config/config.exs`:
```elixir
import Config
config :static_blog, :app, :my_blog
config :static_blog, :site,
title: "My Blog",
tagline: "Writing about things.",
description: "A personal blog about software engineering.",
author: "Your Name",
base_url: "https://blog.example.com",
language: "en"
```
Create a post at `priv/posts/2026-04-11-hello-world.md`:
```markdown
%{
title: "Hello World",
tags: ["intro"],
description: "My first post."
}
---
Welcome to my blog. This is the first post.
```
Build and preview:
```bash
mix blog.build
mix blog.serve
# Open http://localhost:4000/
```
## Configuration
All configuration lives under the `:static_blog` application key.
### Required
| Key | Type | Purpose |
|---|---|---|
| `:app` | atom | Consumer OTP application name. Used for `Application.app_dir/2` to locate `priv/posts/` and `priv/static/`. |
| `:site` | keyword | Site metadata (see below). |
### Site metadata (`:site`)
| Key | Required | Default | Purpose |
|---|---|---|---|
| `:title` | yes | -- | Site name, used in masthead, `<title>`, RSS, and structured data. |
| `:tagline` | yes | -- | Subtitle displayed below the site title. |
| `:description` | yes | -- | Meta description for the home page and RSS channel. |
| `:author` | yes | -- | Default author name. Used when a post has no explicit author. |
| `:base_url` | yes | -- | Production URL (no trailing slash). Used for canonical URLs, Open Graph, and RSS links. |
| `:language` | yes | -- | ISO 639-1 language code (e.g. `"en"`). |
| `:sidebar` | no | `[]` | List of sidebar section maps (see below). |
| `:colophon_body` | no | generic text | Raw HTML string for the colophon page body. |
| `:robots_disallow` | no | `[]` | Extra `Disallow:` paths for `robots.txt`. |
### Sidebar sections
Each sidebar section is a map with a `:heading` and a list of `:links`:
```elixir
config :static_blog, :site,
# ...other keys...
sidebar: [
%{
heading: "Projects",
links: [
%{url: "https://github.com/my-org", label: "My Org", note: "Open source work", rel: "external"},
%{url: "https://example.com", label: "Example"}
]
},
%{
heading: "About",
links: [
%{url: "/colophon/", label: "Colophon"},
%{url: "/feed.xml", label: "RSS feed"}
]
}
]
```
### Optional
| Key | Default | Purpose |
|---|---|---|
| `:template` | `StaticBlog.Template.Default` | Module implementing `StaticBlog.Template` behaviour. |
| `:blog_module` | `nil` | Module with `all_posts/0` for compile-time posts (e.g. your NimblePublisher module). Falls back to `StaticBlog.RuntimePosts.all/0`. |
| `:r2` | -- | R2 publishing config: `bucket:`, `prefix:`, `region:`. Required only for `mix blog.publish`. |
| `:r2_env_vars` | `%{}` | Override env var names for R2 credentials. Keys: `:account_id`, `:access_key_id`, `:secret_access_key`. Defaults to `"R2_ACCOUNT_ID"`, `"R2_ACCESS_KEY_ID"`, `"R2_SECRET_ACCESS_KEY"`. |
| `:auth_token_env_var` | `"BLOG_TOKEN"` | Environment variable name for the Micropub/XML-RPC bearer token. |
| `:preview_port` | `4000` | Default TCP port for the static preview server (`mix blog.serve` and `mix blog.server`). CLI flags override. |
| `:api_port` | `4010` | Default TCP port for the Micropub/XML-RPC server (`mix blog.server`). CLI flags override. |
| `:micropub_url` | `<base_url>/micropub` | Override the Micropub endpoint URL advertised in HTML. Set automatically by `mix blog.server`. |
## Mix tasks
| Command | Purpose |
|---|---|
| `mix blog.build [--output DIR]` | Generate the static site into `_site/` (default). |
| `mix blog.serve [--port 4000] [--dir _site]` | Preview the built site with Erlang's `:inets` httpd. |
| `mix blog.server [--api-port 4010] [--preview-port 4000]` | Start both the static preview server and the Micropub/XML-RPC editing server. |
| `mix blog.publish [--output DIR] [--skip-build]` | Build and sync to Cloudflare R2. |
## Post format
Posts are markdown files in `priv/posts/` named `YYYY-MM-DD-slug.md`. The date and slug are parsed from the filename. Metadata is an Elixir map above a `---` separator:
```markdown
%{
title: "My Post Title",
author: "Author Name",
tags: ["elixir", "release"],
description: "Optional summary for the index page and meta tags.",
status: "published",
published: "2026-04-11T12:00:00Z",
updated: "2026-04-11T14:00:00Z"
}
---
The markdown body starts here.
```
* `:title` is required.
* `:author` is optional; falls back to `site[:author]`.
* `:tags` defaults to `[]`.
* `:status` defaults to `:published`. Set to `"draft"` to exclude from the built site (drafts are still visible in MarsEdit).
* `:published` and `:updated` are optional ISO 8601 timestamps. Default to noon UTC on the filename date.
## Template behaviour
Implement `StaticBlog.Template` to provide custom page rendering:
```elixir
defmodule MyBlog.Template do
@behaviour StaticBlog.Template
@impl true
def index(posts, site), do: # ... return HTML binary
@impl true
def post(post, site), do: # ...
@impl true
def category(tag, posts, site), do: # ...
@impl true
def colophon(site), do: # ...
@impl true
def not_found(site), do: # ...
end
```
Then configure it:
```elixir
config :static_blog, :template, MyBlog.Template
```
The default template (`StaticBlog.Template.Default`) uses Phoenix components and HEEx, renders a Daring Fireball-inspired default layout with light/dark theme toggle, JSON-LD structured data, and full Open Graph / Twitter Card metadata.
## Guides
* [Workflow](guides/workflow.md) -- day-to-day writing, building, and publishing.
* [MarsEdit configuration](guides/marsedit_configuration.md) -- setting up MarsEdit as an editing client.
* [Cloudflare R2 setup](guides/r2_setup.md) -- creating a bucket and configuring credentials.
## License
Apache 2.0. See [LICENSE.md](LICENSE.md).