$ cat posts/making-this-blog.md
Making This Blog with Elixir and Phoenix
I wanted a personal site with a blog and I like experimenting with new tech. Elixir and Phoenix have been on my radar for a while. The main thing I wanted from a blog was the ability to write pure markdown with code blocks and mermaid diagrams and nothing else.
NimblePublisher is a library that reads markdown files at compile time and turns them into Elixir structs.
You drop a .md file in priv/posts/, run mix compile, and it becomes data your app can query. No database needed.
defmodule Website.Blog do
alias Website.Blog.Post
use NimblePublisher,
build: Post,
from: Application.app_dir(:website, "priv/posts/**/*.md"),
as: :posts,
html_converter: Website.Blog.MarkdownConverter
@posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
def all_posts, do: @posts
def get_post_by_id!(id) do
Enum.find(all_posts(), &(&1.id == id)) ||
raise Website.Blog.NotFoundError, "post not found: #{id}"
end
end
Each post file encodes its slug and date in the filename. A file at priv/posts/2026/02-28-making-this-blog.md becomes a post with id "making-this-blog" and date ~D[2026-02-28].
Markdown rendering
MDEx is a Rust-based markdown parser for Elixir. It handles syntax highlighting at compile time by inlining styles directly into the HTML.
MDExMermex does the same for Mermaid diagrams, rendering them to inline SVG at compile time.
MDEx.to_html!(body,
extension: [
strikethrough: true, table: true,
autolink: true, tasklist: true, header_ids: ""
],
render: [unsafe: true],
syntax_highlight: [formatter: {:html_inline, theme: "onedark"}],
plugins: [
{MDExMermex, output: :inline_svg, inject_css: false, inject_js: false}
]
)
Everything you see on the page, highlighted code blocks and the diagram below, was rendered during compilation.
Deployment
This blog runs on Cloudflare Containers, which is obviously overkill for a blog but something I wanted to try. The downside is cold starts — the container sleeps after two minutes of inactivity. The upside is that a 100% server-side rendered blog is extremely cacheable, so it doesn't really matter. A custom plug sets CDN cache headers and most requests are served from the nearest Cloudflare edge node without ever waking the container.