README.md

# 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).