Articles

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.

3 min
  • elixir
  • phoenix
Read in Português

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.

1 person is here