I recently decided to code a new simple website for myself (you’re looking at it). My old website was a simple static generated website, which is awesome for a lot of purposes. But I wanted my website to be more of a real application that I can use for more than simple blog posts. So I decided to implement it in Elixir using the Phoenix framework.

I still wanted to be able to write my blog posts using simple Markdown documents that I version control in Git. I prefer using static Markdown documents over content loaded from a database since I like using my normal code editor to write in, I get version control for free and I don’t have to think about an admin panel.

In this post I’m gonna share how I implemented a very simple static blog engine in Elixir with Markdown support.

What we want

Let’s start by thinking about what goals we have for our blog engine.

I want each post to be a single Markdown file with Frontmatter in YAML. Example:

---
title: Markdown for blog posts is nice, says Sebastian
date: 2016-05-01
intro: Read about why Markdown is healthy for you.
---

All the **Markdown** here...

We can put the Markdown files in the priv/posts folder. The name of each file will also be the slug on the website. Example: priv/posts/my-blog-post.md results in a blog post at http://example.com/my-blog-post.

If a post needs to refer to static assets, such as images, we can put them in web/static/assets/images and let Phoenix serve them.

We want every request to be as fast as possible, so we only want to render the Markdown files once. In other words we need to keep the compiled posts in memory somehow.

Alright, now let’s code some Elixir!

Create a new Phoenix application

We’ll start by firing up a new Phoenix app. We won’t use Ecto for simplicity.

mix phoenix.new static_blog --no-ecto

We’re going to need a few dependencies:

  • yamerl: An Erlang YAML parser.
  • Earmark: An Elixir Markdown parser.
  • Timex: An Elixir date/time library.

Add the following lines to your mix.exs’s deps function:

defp deps do
  [
   # ...
   {:earmark, "~> 0.2.0"},
   {:timex, "~> 2.1.4"},
   {:yamerl, github: "yakaz/yamerl"}]
end

Now run mix deps.get from your Terminal.

Also add :timex and :yamerl to your applications in mix.exs:

def application do
  [mod: {StaticBlog, []},
   applications: [:phoenix, :phoenix_html, :cowboy, :logger, :gettext,
                  :timex, :yamerl]]
end

We should also add at least two posts so we have some content to work on. See the two posts I made for example.

Write a module that compiles a single post

We’re going to define a module named StaticBlog.Post, which:

  • Defines a struct that represents a blog post.
  • Defines a compile/1 function that takes the name of a Markdown file and returns a Post struct.

Let’s start by defining the struct:

defmodule StaticBlog.Post do
  defstruct slug: "", title: "", date: "", intro: "", content: ""
end

Next we’ll define the compile/1 function:

defmodule StaticBlog.Post do
  # ...

  def compile(file) do
    post = %StaticBlog.Post{
      slug: file_to_slug(file)
    }

    Path.join(["priv/posts", file])
    |> File.read!
    |> split
    |> extract(post)
  end
end

It takes a file argument, which could be fx my-blog-post.md. It then creates a new Post struct and sets the slug key using a file_to_slug/1 function, which we’ll define next.

Finally it builds the full path to the file using Path.join/1 (results in fx priv/posts/my-blog-post.md), which is piped to File.read!/1. The content of the file is then piped to split/1, whose result is again piped to extract/2. We’ll define split/1 and extract/2 momentarily.

extract/2 takes the post as its second argument and will also return a Post struct, which means our compile/1 function will return the final Post value. Remember that Elixir has implicit returns, so the result of the last executed statement is automatically returned.

The file_to_slug/1 function is super simple:

defmodule StaticBlog.Post do
  # ...

  defp file_to_slug(file) do
    String.replace(file, ~r/\.md$/, "")
  end
end

Could we have avoided creating this function and put it directly in place of file_to_slug(file) inside compile/1? Yes we could, but it’s much nicer to extract it into its own function. It makes the intention crystal clear vs. an odd String.replace call.

split/1 takes file data and returns a tuple of parsed Frontmatter and compiled Markdown. It looks like this:

defmodule StaticBlog.Post do
  # ...

  defp split(data) do
    [frontmatter, markdown] = String.split(data, ~r/\n-{3,}\n/, parts: 2)
    {parse_yaml(frontmatter), Earmark.to_html(markdown)}
  end
end

It uses String.split/3 to split the file data string in two parts (note the parts: 2, which limits to only splitting once = two parts). The regex ~r/\n-{3,}\n/ looks for the first line that consists of min. 3 dashes and has a newline before and after it. We then pattern match to get the frontmatter and the markdown into two separate variables. We parse both using parse_yaml/1 and Earmark.to_html/1 respectively and return a tuple with the two elements.

parse_yaml/1 is a little helper function. Yamerl’s :yamerl_constr.string/1 returns a list, where we’re only interested in the first element:

defmodule StaticBlog.Post do
  # ...

  defp parse_yaml(yaml) do
    [parsed] = :yamerl_constr.string(yaml)
    parsed
  end
end

Now we can define extract/2. It takes a tuple with parsed Frontmatter properties and the parsed markdown (now HTML) and the Post that it should populate with the remaining values.

defmodule StaticBlog.Post do
  # ...

  defp extract({props, content}, post) do
    %{post |
      title: get_prop(props, "title"),
      date: Timex.parse!(get_prop(props, "date"), "{ISOdate}"),
      intro: get_prop(props, "intro"),
      content: content}
  end
end

We simply return a new Post struct that inherits the given post and adds the title, date, intro and content attributes.

Note that the result from :yamerl_constr.string/1 we got earlier, props, is an Erlang property list, which are a little awkward to work with in Elixir. So we define the function get_prop/2 that helps us:

defmodule StaticBlog.Post do
  # ...

  defp get_prop(props, key) do
    case :proplists.get_value(String.to_char_list(key), props) do
      :undefined -> nil
      x -> to_string(x)
    end
  end
end

It uses :proplists.get_value, which expects the key to be a char list. It will return the :undefined atom if the given key is not defined - we prefer a nil. And if the key does exist it returns a char list, which we convert to a string.

See complete StaticBlog.Post code.

We’re now ready to try out our post compiler. Fire up iex and run:

$ iex -S mix

iex(1)> StaticBlog.Post.compile("first-post.md")
%StaticBlog.Post{content: "<p>This post is written in <strong>Markdown</strong>.</p>\n<h2>A header2 is good</h2>\n<p>Lists are nice, too:</p>\n<ul>\n<li>Apples\n</li>\n<li>Bananas\n</li>\n<li>Pears\n</li>\n</ul>\n",
 date: #<DateTime(2016-04-24T00:00:00Z)>,
 intro: "The first post is about one topic.", slug: "first-post",
 title: "First post"}

Great success! Next step is to write a crawler that will discover all our posts and compile them all.

Crawling posts

We will define another module, StaticBlog.Crawler, which defines a crawl/0 function that finds all our blog posts and returns a list of Post structs. It’s remarkably simple:

defmodule StaticBlog.Crawler do
  def crawl do
    File.ls!("priv/posts")
    |> Enum.map(&StaticBlog.Post.compile/1)
    |> Enum.sort(&sort/2)
  end

  def sort(a, b) do
    Timex.compare(a.date, b.date) > 0
  end
end

It lists all files in our priv/posts folder, compiles each file using StaticBlog.Post.compile/1 and sorts them chronologically (newest first).

Let’s try it out. Back to iex (remember to restart iex between editing code in lib):

$ iex -S mix

iex(1)> StaticBlog.Crawler.crawl
[%StaticBlog.Post{content: "<p>This post is also written in <strong>Markdown</strong>.</p>\n<p>A link: <a href=\"http://www.sebastianseilund.com\">Sebastian Seilund</a></p>\n<p>![phoenix][/images/phoenix.png]</p>\n",
  date: #<DateTime(2016-05-02T00:00:00Z)>,
  intro: "The second post is about another topic.", slug: "second-post",
  title: "Second post"},
 %StaticBlog.Post{content: "<p>This post is written in <strong>Markdown</strong>.</p>\n<h2>A header2 is good</h2>\n<p>Lists are nice, too:</p>\n<ul>\n<li>Apples\n</li>\n<li>Bananas\n</li>\n<li>Pears\n</li>\n</ul>\n",
  date: #<DateTime(2016-04-24T00:00:00Z)>,
  intro: "The first post is about one topic.", slug: "first-post",
  title: "First post"}]

Sweet! A list with two blog posts. Who would have known?

Building a repository using GenServer

Okay, so we now have a way to get a list of all our posts. But one of our initial goals was to only compile the posts once. We need something in our application that can run StaticBlog.Crawler.crawl/0 once, hold on to the returned list and respond to fx get_by_slug/1 and list/0 calls.

This sounds like a good fit for OTP’s GenServer. I’m not gonna go into depth with GenServer here since there are many good resources available for that - see this Elixir Getting Started guide fx. Basically our GenServer will be a part of our application’s supervision tree and it will hold the list of posts and respond to get/list calls.

Let’s call our module StaticBlog.Repo. We start by useing GenServer and implementing the start_link/0 function so our application knows how to start it:

defmodule StaticBlog.Repo do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
  end

  def init(:ok) do
    posts = StaticBlog.Crawler.crawl
    {:ok, posts}
  end
end

We use name: __MODULE__ since there will ever only be one StaticBlog.Repo process running in our system. We don’t care about redundancy at this point, plus if the process were to crash, the OTP supervisor would bring up a new one right away.

We also implemented the init/1 function that OTP calls to initialize the state of our repo. init/1 accepts an :ok atom, since that’s what supplied as the second argument to GenServer.start_link/3. It doesn’t really matter in our example what we use here. In the function body we call StaticBlog.Crawler.crawl and return with a tuple of :ok and the list of posts. This is how we signal to OTP that our GenServer started properly and to tell it what the current state is. Our state is always our list of posts - it never changes while the application is running (we don’t have any live-reload functionality yet).

Other parts of our application should be able to query our blog posts. We will settle on supporting a get_by_slug/1 method to get a single post by it’s URL slug, and list/0 which returns all posts. We add the following functions to our module:

defmodule StaticBlog.Repo do
  # ...

  def get_by_slug(slug) do
    GenServer.call(__MODULE__, {:get_by_slug, slug})
  end

  def list() do
    GenServer.call(__MODULE__, {:list})
  end

  def handle_call({:get_by_slug, slug}, _from, posts) do
    case Enum.find(posts, fn(x) -> x.slug == slug end) do
      nil -> {:reply, :not_found, posts}
      post -> {:reply, {:ok, post}, posts}
    end
  end

  def handle_call({:list}, _from, posts) do
    {:reply, {:ok, posts}, posts}
  end
end

The two first functions, get_by_slug/1 and list/0, simply calls our repo process with the given request. The first argument, __MODULE__ is what we named our process in start_link/0. The second argument represents what the caller asked for, i.e. either {:list} or {:get_by_slug, slug}.

We then implement handle_call for each of the two types of messages. The first one replies {:ok, post} if a post with the given slug was found, and :not_found otherwise. The second one always replies with all the posts. Both of them returns the posts list as the third return tuple element, which means that we keep using the same list of posts.

See complete StaticBlog.Repo code.

Now we just have to add our StaticBlog.Repo to our application’s supervision tree.

Open lib/static_blog.ex and add a worker to the children list. Like this:

def start(_type, _args) do
  import Supervisor.Spec, warn: false

  children = [
    supervisor(StaticBlog.Endpoint, []),
    worker(StaticBlog.Repo, []), # <--- THIS IS THE LINE WE ADDED
  ]

  opts = [strategy: :one_for_one, name: StaticBlog.Supervisor]
  Supervisor.start_link(children, opts)
end

Let’s try it out. Back to iex:

$ iex -S mix

iex(1)> StaticBlog.Repo.get_by_slug("first-post")
{:ok,
%StaticBlog.Post{...content omitted...}

iex(2)> StaticBlog.Repo.list()
{:ok, ...content omitted...}

Yay, it worked. Just for the fun of it, let’s try to manually kill the process to make sure that we configured our supervision tree correctly:

$ iex -S mix

iex(1)> Process.whereis(StaticBlog.Repo) |> Process.exit(:kill)
true

iex(2)> StaticBlog.Repo.list()
{:ok, ...content omitted...}

It quickly recovered automatically. Isn’t Elixir/Erlang just amazing?

Okay, all we have left now is to actually serve some blog posts.

Show post on website

First we add a route handler to StaticBlog.Router (web/router.ex):

get "/:slug", PostController, :show

Make sure to put this line after any other routes you may already have as it will catch all requests at the first directory level.

We add our controller to web/controllers/post_controller.ex:

defmodule StaticBlog.PostController do
  use StaticBlog.Web, :controller

  def show(conn, %{"slug" => slug}) do<
    case StaticBlog.Repo.get_by_slug(slug) do
      {:ok, post} -> render conn, "show.html", post: post
      :not_found -> not_found(conn)
    end
  end

  def not_found(conn) do
    conn
    |> put_status(:not_found)
    |> render(StaticBlog.ErrorView, "404.html")
  end
end

It takes the slug from the params, queries it from the repo and renders the show.html template if found, or responds with a 404 page if it was not found.

We also need to implement a view, which does nothing. Add web/views/post_view.ex:

defmodule StaticBlog.PostView do
  use StaticBlog.Web, :view
end

Finally we add our HTML template to web/templates/post/show.html.eex:

<p><a href="<%= page_path(@conn, :index) %>">&laquo; Back to index</a></p>
<article>
  <h1><%= @post.title %></h1>
  <p><%= Timex.format!(@post.date, "{Mshort} {D} {YYYY}") %></p>
  <%= raw(@post.content) %>
</article>

It links back to the index page and then renders the title, date and HTML content of the post. Notice how we use the raw/1 function. This is because @post.content is already rendered HTML, and we don’t want Phoenix to escape it.

Now start Phoenix with:

mix phoenix.server

Go to http://localhost:4000/first-post or http://localhost:4000/second-post. The posts should appear. And if you go to http://localhost:4000/third-post you should see the standard 404 page.

Listing posts on the frontpage (last step)

Since we already have a frontpage (index), all we have to do is supply the list of posts to the index template and render some HTML.

Change the index/2 function in web/controllers/page_controller.ex to this:

def index(conn, _params) do
  {:ok, posts} = StaticBlog.Repo.list()
  render conn, "index.html", posts: posts
end

And then replace web/templates/page/index.html.eex with this:

<%= for post <- @posts do %>
  <article class="row">
    <h2><%= post.title %></h2>
    <p><%= Timex.format!(post.date, "{Mshort} {D} {YYYY}") %></p>
    <p><%= post.intro %></p>
    <p><a href="<%= post_path(@conn, :show, post.slug) %>">Read post</a></p>
  </article>
<% end %>

We loop over each post and render its title, date, intro and a link to the post itself. Phoenix automatically makes a post_path/3 helper function for us, which makes linking easy.

Start Phoenix again with mix phoenix.server and go to http://localhost:4000/ to see a list of all your blog posts.

Conclusion

We now have a simple way for our Markdown blog posts to live in our existing Phoenix application. Adding a new blog post is as simple as adding another Markdown file. The posts are only rendered once, and are served at sub-millisecond speed by Elixir. Exactly what we wanted.

You could extend the system further, such as add pagination, a search feature, tags etc. All you would need to do is adjust the StaticBlog.Post struct, add support for new query methods in StaticBlog.Repo and adjust your routes/templates.

You may think this solution is a little overkill for a simple blog. But then again. It’s not that much code. And now you have complete control over it and don’t have to deal with a beast such as Wordpress. I sure like it. Plus now I can use my website for other fun things because of Phoenix.

The complete source code is available on GitHub. If you want to deploy your code in a very easy way, the Phoenix Heroku guide is very helpful.

If you have any comments, I’d love to hear from you.