Bego Solutions

How this blog is setup

The basic overview

This website, not including the external links to my games showcase and my Permaplanner side project is a simple phoenix application, using plain static HTML, tailwind and esbuild.

NimblePublisher is an extremely lighweight elixir library that allows you to write blog posts as markdown files. This is great because, while they are files in your repository, they do not actually require disk storage when deployed to prod. Rather, the way the html is generated from markdown makes the articles part of compilation process and thus part of the code that’s running your app.

Integration of it is quite simple and there’s plenty of info in hexdocs on various things you can do.

For my purpose, I needed to solve a few extra things

  • getting it integrated into the app layout
  • getting it to behave with tailwind
  • getting markdown support into post description

Integration into app layout

The basic integration was as straightforward as the docs said.

I created a Bego.Blog.Post exactly as instructed in the docs

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

  def build(filename, attrs, body) do
    [year, month_day_id] = 
      filename 
      |> Path.rootname() 
      |> Path.split() 
      |> Enum.take(-2)
    [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] ++ Map.to_list(attrs)
    )
  end
end

I created a Bego.Blog as a context to fetch a list of posts and an individual post

defmodule Bego.Blog do
  use NimblePublisher,
    build: Bego.Blog.Post,
    from: Application.app_dir(:bego, "priv/blog/**/*.md"),
    as: :posts,
    highlighters: [:makeup_elixir, :makeup_erlang]

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

  def list_posts() do
    @posts
  end

  def find_by_id(slug) do
    Enum.find(@posts, &(&1.id == slug))
  end
end

Then, I added a route and a controller:

# router.ex

get "/blog", BlogController, :index
get "/blog/:id", BlogController, :show

# controller 

defmodule BegoWeb.BlogController do
  use BegoWeb, :controller

  alias Bego.Blog

  def index(conn, _) do
    posts = Blog.list_posts()
    render(conn, "index.html", posts: posts)
  end

  def show(conn, %{"id" => id}) do
    post = Blog.find_by_id(id)
    render(conn, "show.html", post: post)
  end
end

This allowed me to write .md files inside priv/{year}/{month}-{day}-{id}.md that automatically become renderable blog posts using an html module.

defmodule BegoWeb.BlogHTML do
  use BegoWeb, :html

  def render("index.html", assigns) do
    ~H"""
    <ul class="w-full">
      <li :for={post <- @posts} class="flex flex-col justify-start">
        <article class="flex flex-col">
          <h1 class="font-bold text-lg">
            <a class="text-blue-900 visited:text-blue-800" href={~p"/blog/#{post.id}"}>
              <%= post.title %>
            </a>
          </h1>
          <span class="text-sm text-gray-700 font-bold"><%= post.date %></span>
          <span class="text-gray-900"><%= post.description %></span>
        </article>
      </li>
    </ul>
    """
  end

  def render("show.html", assigns) do
    ~H"""
    <h1 class="font-bold text-lg pb-8"><%= @post.title %></h1>
    <article class="post">
      <%= Phoenix.HTML.raw(@post.body) %>
    </article>
    """
  end
end

Getting it to behave with tailwind

Using tailwind for the structure is easy enough, as you can tell in the example above.

However, for the post body, I’m using Phoenix.HTML.Raw to get the generated post body rendered. This is great for the content, but I can’t exactly add tailwind classes to it.

What I can do, is use tailwind’s @apply statement to get plain html treated as if it has tailwind classes.

/* app.css */


.post h1 {
  @apply py-2 font-sans text-lg font-bold;
}

.post h2 {
  @apply py-2 font-sans text-lg font-bold;
}

.post h3 {
  @apply py-2 font-sans text-lg font-bold;
}

.post p {
  @apply py-4;
}

.post a {
  @apply text-blue-500;
}

/* etc. */

This is just the basic initial stuff, but of course, you can extend this as far as you want.

It’s not the nicest. I didn’t quite figure out how to separate the main css out with import statements, but I can figure that out later. It works for now.

Markdown support in the post description

This one was extremely straightforward. I just slightly tweaked post.ex

defmodule Bego.Blog.Post do
  # ...

  def build(filename, attrs, body) do
    [year, month_day_id] = 
      filename 
      |> Path.rootname() 
      |> Path.split() 
      |> Enum.take(-2)
    [month, day, id] = String.split(month_day_id, "-", parts: 3)
    date = Date.from_iso8601!("#{year}-#{month}-#{day}")
    attrs = Map.put(
      attrs, 
      :description, 
      Earmark.as_html!(attrs.description)
    )
    struct!(
      __MODULE__, 
      [id: id, date: date, body: body] ++ Map.to_list(attrs)
    )
  end
end

That’s all there is to it. Any markdown in the description field is now parsed into html.

%{
  title: "How this blog is setup",
  author: "@begedinnikola",
  tags: ~w(hello),
  description: "An overview how how this blog integrates into the **Phoenix** application that is this website."
}
---
# The basic overview

This website, not including the external

...etc.

What’s left?

I didn’t quite get synthax higlighting. I have it built in for elixir, since NimblePublisher relis internally on Earmark and then uses Makeup for code highlighting.

The thing is Makeup doesn’t support a lot of languages and I’m not solely an elixir engineer, so I will need more at some point soon.

It works for now, though.