#elixir #phoenix #org-mode #nimble-publisher

Building a Blog with Phoenix and Org-mode

How I set up NimblePublisher to accept both Markdown and Org-mode files, and why Pandoc is the right tool for the conversion pipeline.


Why Org-mode?

I have been writing in Org-mode for years. My notes, my tasks, my journals — all of it lives in plain .org files on disk. When I decided to start a blog built on Phoenix, I did not want to switch to Markdown just because it is the default.

The good news: with Pandoc and NimblePublisher, you do not have to.

The Pipeline

The conversion happens at compile time, not at runtime. This is the key insight that makes the whole thing practical.

defmodule OnemargaroBlog.Post do
  def build(filename, attrs, body) do
    html_body =
      case Path.extname(filename) do
        ".org" -> OnemargaroBlog.OrgConverter.to_html!(body)
        _      -> body
      end

    %__MODULE__{
      slug:  extract_slug(filename),
      body:  html_body,
      title: attrs["title"],
      date:  parse_date(filename)
    }
  end
end

When you run mix phx.server, NimblePublisher walks priv/posts/, calls Post.build/3 for every file it finds, and stores the results in a module attribute. Zero database queries at runtime.

Setting Up Pandoc

Pandoc is a system binary, not an Elixir library. You call it with System.cmd/3.

defmodule OnemargaroBlog.OrgConverter do
  @flags ~w[-f org -t html5 --no-highlight --wrap=none]

  def to_html!(content) do
    case System.cmd("pandoc", @flags, input: content, stderr_to_stdout: true) do
      {html, 0} -> String.trim(html)
      {err,  _} -> raise "Pandoc failed: #{err}"
    end
  end
end

Install it once per environment:

# macOS
brew install pandoc

# Ubuntu / Debian
sudo apt install pandoc

# Dockerfile (build stage only — not needed at runtime)
RUN apt-get install -y pandoc

Org Syntax You Can Use

Headings

Org headings use asterisks. The number of asterisks sets the level.

* H1
** H2
*** H3

Pandoc maps these to <h1>, <h2>, <h3> in the HTML output.

Inline markup

Org syntax Result
*bold* bold
/italic/ italic
~code~ code
+strikethrough+ strikethrough
[[url][label]] hyperlink

Code blocks

Use #+BEGIN_SRC with the language name for syntax-highlighted blocks:

defmodule Hello do
  def world, do: IO.puts("Hello, world!")
end
mix phx.server

Blockquotes

The purpose of a programming language is to let you express ideas clearly. If you cannot say something clearly, you probably do not understand it yet.

Lists

Unordered:

  • NimblePublisher handles the file pipeline
  • Earmark parses Markdown
  • Pandoc converts Org-mode
  • Phoenix serves everything

Ordered:

  1. Create a .org file in priv/posts/
  2. Add the Elixir frontmatter block at the top
  3. Write the body in Org syntax
  4. Restart the server — the post appears

Tables

Org tables use | for columns and - for separators. Pandoc renders them as proper HTML <table> elements.

Feature Markdown Org-mode
Tables Limited Native
Code blocks Fenced BEGINSRC
Task lists GitHub only Native
Links [text](url) [[url][text]]
Export targets HTML HTML, PDF, LaTeX, ODT

Frontmatter Format

NimblePublisher reads the block between the first %{ and --- as Elixir map syntax, not YAML. This is important — it means the values are native Elixir terms.

%{
  title: "My Post Title",
  description: "A short summary shown in post cards.",
  tags: ["elixir", "phoenix"]
}
---

The filename drives the date and slug automatically:

2024-06-15-building-with-phoenix.org
     │          │
  date        slug → /blog/building-with-phoenix

Limitations

Pandoc does not produce Phoenix-aware HTML. A few things to keep in mind:

  • Verified routes (\~p"/blog/slug") do not work inside Org files. Use plain https:// URLs when linking to other posts.
  • Syntax highlighting — pass --no-highlight to Pandoc and let makeup_elixir handle it via NimblePublisher's highlighter pipeline.
  • Images — reference them as /images/filename.png and place the files in priv/static/images/.

Summary

The full pipeline is three steps:

  1. Write .org files in priv/posts/
  2. OrgConverter.to_html!/1 calls Pandoc at compile time
  3. Phoenix serves the resulting HTML — no Pandoc needed in production

The same .post-card and .prose CSS classes from blog.css style everything, whether the source was Markdown or Org.