How this site was built
A Phoenix blog with no database: NimblePublisher reads Markdown articles at compile time and they become part of the binary.
This site has no database. Articles are Markdown files in the repository, read at compile time and baked into the compiled binary. Serving an article is a lookup in a list that already lives in memory.
What does the work
The core piece is NimblePublisher, from the Dashbit team. It walks a directory, converts each file to HTML, and calls a build function for each one:
defmodule Portfolio.Blog do
alias Portfolio.Blog.Post
use NimblePublisher,
build: Post,
from: Application.app_dir(:portfolio, "priv/posts/**/*.md"),
as: :posts,
highlighters: [:makeup_elixir, :makeup_erlang]
@posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
def all(locale), do: Enum.filter(@posts, &(&1.locale == locale))
end
@posts is an ordinary module attribute. Once the module compiles, it is a constant, and reading a post never touches a query or a cache.
The file path is metadata
Every post lives at priv/posts/{locale}/{year}/{month-day}-{slug}.md. Language, date and slug come from the path; everything else comes from the frontmatter at the top of the file:
[locale, year, month_day_id] =
filename |> Path.rootname() |> Path.split() |> Enum.take(-3)
Renaming a file therefore changes the article's URL. The coupling is deliberate: the repository becomes the source of truth about what is published, and git log tells the editorial history of the blog.
What this costs
| Markdown in the repo | CMS with a database | |
|---|---|---|
| Publishing | git push |
Save in the admin |
| Writing from another device | Needs the repo | Any browser |
| Revision history | git log |
Depends on the CMS |
| Infrastructure cost | Web server only | Server + database |
| Response time | In-memory list | Database query |
The real trade-off is the second row: writing requires the repository at hand. For a personal blog belonging to someone who already lives inside git, that is a low price for what you get back in simplicity.
One note on recompilation
Because articles are read at compile time, editing a .md has to trigger a module recompile. NimblePublisher registers each file as an @external_resource, so Mix already knows. In development, it is worth teaching Phoenix to reload the browser too:
# config/dev.exs
live_reload: [
patterns: [
~r"priv/posts/.*(md)$"
]
]
Save the file and the open page refreshes itself.