Go back

Simon Ricard

2024-05-30


It’s alive!

My very first post with my custom static site generator using Elixir.

I’ve seen many devs having their own little platforms for sharing their thoughts and ideas, and I decided that I could do the same here!

I’ve been mostly developing with Elixir for the past few years, and so I wanted to try and create a static site generated from markdown files with it. The result is something that could be deployed litterally anywhere we can deploy static files (S3, Vercel, Github Pages, etc) while only writing Elixir, Markdown and some Javascript.

Here is the repo for the portfolio itself. Depending on when you’ll be reading this article, this might very well still be a WIP.

Key pieces

Here are the main pieces for this statically generated blog website.

How?

While I could go into lots of details with how this whole website has been created, here are the most important part.

This part is where we define the location of our posts, as well as the code highlighter that will be used:

defmodule Website.Blog do
  use NimblePublisher,
    build: Website.Post,
    from: "./posts/**/*.md",
    as: :posts,
    highlighters: [:makeup_elixir, :makeup_erlang]

  @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})

  def all_posts, do: @posts
  [...]
end

Here is where we will define what our post will look like, what its metadata will be and other things of interest:

defmodule Website.Post do
  @enforce_keys [:id, :author, :title, :body, :description, :tags, :date, :path]
  defstruct [:id, :author, :title, :body, :description, :tags, :date, :path]

  def build(filename, attrs, body) do
    path = Path.rootname(filename)
    [year, month_day_id] = path |> Path.split() |> Enum.take(-2)

    path = path <> ".html"
    [month, day, id] = String.split(month_day_id, "-", parts: 3)

    date = Date.from_iso8601!("#{year}-#{month}-#{day}")

    struct!(__MODULE__, [id: id, date: date, body: body, path: path] ++ Map.to_list(attrs))
  end
end

This is where we actually convert the markdown to HTML. We first fetch all of the posts and pass them down to the index page so that they can be listed eventually in a list. Then, we will generate an html page for every single of of those posts.

defmodule Website.Build do
  @output_dir "./output"
  def run() do
    File.mkdir_p!(@output_dir)
    posts = Website.Blog.all_posts()

    render_file("index.html", Website.index(%{posts: posts}))

    for post <- posts do
      dir = Path.dirname(post.path)

      if dir != "." do
        File.mkdir_p!(Path.join([@output_dir, dir]))
      end

      render_file(post.path, Website.post(%{post: post}))
    end

    :ok
  end

  defp render_file(path, rendered) do
    safe = Phoenix.HTML.Safe.to_iodata(rendered)
    output = Path.join([@output_dir, path])
    File.write!(output, safe)
  end
end

And that’s it! We run this run() function via a mix task, along with the other stuff that needs to be generated (Tailwind, esbuild, etc).

There are some other stuff going on for the photos and the other sections, but I believe this is a lot less interesting.

If you want to take a look at the repo, feel free to do so!