Posts | About

Building this blog

elixir

Posted on 2023-12-17 by Matt.

There are a lot of options in 2023

I've been back-burnering the thought about starting a blog for a while now. A new team member suggested I start one and so I went for it.

At first, I was looking at a few existing tools, when I came across Crafting your own Static Site Generator using Phoenix. I figured, yup, this is all I really want - just convert some markdown files, feed them into heex templates, and call it a day.

Getting Started

# lib/blog.ex
defmodule Blog do
  alias Blog.Post

  use NimblePublisher,
    build: Post,
    from: "./posts/**/*.md",
    as: :posts,

  ...
end

# lib/post.ex
defmodule Blog.Post do
  ...
  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

Just following along with the above post, it didn't take long to get the fundamentals working. Most of the work is handled by NimblePublisher, we just create a Blog.Post module that defines a struct and provides a build/3 function. build/3 creates a Post struct for each markdown file in posts.

Our Blog.Site module then passes those through our heex template, generating the html files and writing them to our output dir. Blog.Site also builds our about page as well as our index.html, complete with a list of links for each post. Lastly, we add a mix task to trigger this build process.

Honestly, we could have stopped here, the basics are handled. We can take our dir of markdown posts and generate our static site.

Refactoring and Extending

I'd rather have my templates in files like index.html.heex instead of defined inline as functions in Blog.Site. Extracting them was easy, realizing we needed to call embed_templates("templates/*") was most of the work.

This is one of the double-edged swords about Phoenix - when you generate a project, so many things like this are taken care of for you and then also hidden in __using__ macros that you might use the framework for a long time and never know how these pieces of the puzzle fit it. The good news is that it's usually all there in your code, and you're only one go-to-definition away from some answers to Phoenix's small mysteries.

Tailwind Support

Going back to the steps offered by the article, I tried adding some Tailwind support. I have mixed feelings about Tailwind in general, but I have access to TailwindUI and I figured I could snag some nice blog templates to get an instant production value boost. The problem was that the syntax highlighting styles weren't being correctly applied to our code blocks.

Considering this blog is to primarily write about code and software, that's pretty much a deal-breaker. I wasn't too brokenhearted about it, so I reverted the changes that added Tailwind in the first place and went about my business.

diff --git a/config/config.exs b/config/config.exs
deleted file mode 100644
index 57a1117..0000000
--- a/config/config.exs
+++ /dev/null
@@ -1,13 +0,0 @@
-import Config
-
-# Configure tailwind (the version is required)
-config :tailwind,
-  version: "3.3.6",
-  default: [
-    args: ~w(
-      --config=tailwind.config.js
-      --input=css/app.css
-      --output=../output/assets/app.css
-    ),
-    cd: Path.expand("../assets", __DIR__)
-  ]
diff --git a/mix.exs b/mix.exs
index 49ecef6..be88149 100644
--- a/mix.exs
+++ b/mix.exs
@@ -29,14 +29,13 @@ defmodule Blog.MixProject do
       {:makeup_js, "~> 0.1"},
       {:makeup_json, "~> 0.1"},
       {:nimble_publisher, "~> 0.1.3"},
-      {:phoenix_live_view, "~> 0.18"},
-      {:tailwind, "~> 0.1.8"}
+      {:phoenix_live_view, "~> 0.18"}
     ]
   end

   defp aliases do
     [
-      "site.build": ["build", "tailwind default --minify"]
+      "site.build": ["build"]
     ]
   end
 end
diff --git a/mix.lock b/mix.lock
index 5bc0bc4..5c1b9d3 100644
--- a/mix.lock
+++ b/mix.lock
@@ -18,7 +18,6 @@
   "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
   "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"},
   "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
-  "tailwind": {:hex, :tailwind, "0.1.10", "21ed80ae1f411f747ee513470578acaaa1d0eb40170005350c5b0b6d07e2d624", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e0fc474dfa8ed7a4573851ac69c5fd3ca70fbb0a5bada574d1d657ebc6f2f1f1"},
   "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
   "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
   "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},

This is one of the benefits to a proper, disciplined git workflow - when I added Tailwind, those changes were committed on their own, making it trivial to revert or drop altogether. And if I decide to re-instate Tailwind after all, I can just revert the revert and we're good to go.

Red Herring

NimblePublisher uses makeup to provide syntax highlighting for some languages, and I figured that Tailwind's CSS reset was just interfering with this in some way. I had added all the makeup highlighters I could find on hex, but even after removing Tailwind, they didn't seem to be working quite right.

After skimming some docs, I realized that the way makeup works is that you need call some code to actually generate the style sheets in the first place. This made sense, so I went ahead and added some of that to the build step, and our styles were working... sort of.

Turns out, this syntax highlighting didn't work so hot. No biggie, after some quick looking around, I found MDEx, which seems like a better solution overall. It has a bunch of different themes and it supports a ton more languages than makeup did.

To use this, we just needed to add a custom convert/4 function.

defmodule Blog.HTMLConverter do
  def convert(_filepath, body, _atrs, _opts) do
    MDEx.to_html(body)
  end
end

Then we just need to add html_converter: Blog.HTMLConverter to our NimblePublisher options.

This didn't work out at all, it wasn't even calling HTMLConverter.convert/4. After a few sanity checks failed, I went into deps/nimble_publisher and grepped for html_converter. Nothing. This is when I went back to the docs and realized the version of NimblePublisher that I was using was pre-1.0. I'd copied and pasted the line from the article and apparently it was older than I thought.

Anyway, a quick update to ~> 1.0 and we're off to the races. Our custom build/4 function is being called and the code blocks look legit.

A little styling to add some padding and horizontal scrolling if needed, and we're good to go.

QOL - file watcher and local server

By this point, we have a very minimal CSS file, as well as a few links that use an absolute path. This is where we need to introduce some kind of server. I could have very easily converted this to a proper Phoenix app, and maybe I will at some point, but for now, I'm happy to stick with the suggestion made in the post I've been following along with:

cd output
python3 -m http.server 8000

We also don't want to keep tabbing around and typing mix build (okay, realistically, hitting ctrl+r m and hitting enter on the first hit), but we also don't want to over complicate anything. I copied and pasted a watcher.sh from another project. Just needed to tweak the excluded files and the actual command we're running, then boom! We have a watcher that rebuilds on save.

while inotifywait -q --recursive --event modify --exclude "build|output|.git|watcher.sh" .; do
    mix build
done

We had to set the permissions, but this was kind of nice because this is one of the first times I've ever actually remembered the syntax here.

$ chmod u+x watcher.sh

I'm sure there's a better way to handle live reloading, but right now, IDGAF.

$ ./watcher.sh
./posts/2023/ MODIFY 12-17-blog.md
Compiling 1 file (.ex)
BUILT in 16.638ms

Hosting via Github Pages

At first, I was just going to create a simple blog dir on 1-800-rad-dude.com, but I wanted to use a subdomain. This didn't play well with my hosting. For desktop, it was fine, but for mobile, it redirected to the /blog dir, which meant that the absolute paths we're using here are now incorrect. I wanted to be able to keep this separate from everything else there, so I looked into Github Pages.

The official instructions I found were either incorrect or just out of date, they insisted that I create a repo named <username>.github.io. This lead me down a little bit of a rabbit hole. It took some fiddling with Github Actions actions/upload-pages-artifact@v1 and actions/deploy-pages@v3 (and reading the error messages) before I realized what was going on. It turns out that I created a matt-savvy.github.io repo for no reason - the artifact that your actions build is what ends up getting served.

I was able to configure this repo to serve my Github Pages artifact on my own subdomain.

Doing it in style

And now that this is out there to see, I need to make it look tolerable. I'm no graphic designer, but I can style it well enough that no one's going to worry that I forgot to upload my stylesheet in the first place.

The first thing is something that took me way too long to learn about the first time I dealt with it, adding a "viewport" meta tag. Easy to leave out when working on a desktop, but then the second someone opens the link you text them, they need to zoom in like crazy.

<meta name="viewport" content="width=device-width, initial-scale=1" />

Next, we just needed a little bit of margins and padding here and there. Nothing exciting about this, but we all know how intolerable it is when you end up on an article where there is literally zero space between the left edge of the screen and the start of the text.

The code blocks are 90% of the production value, and MDEx took care of that for me.

All that's left is to add a little color. I started with a dark theme, simply grabbing the colors from the nightfox theme I'm already using here. Then, after taking five minutes to finally learn about prefers-color-scheme media queries, I made a light theme based off of the dayfox variation.

body {
    /* default/dark color scheme, taken from nightfox theme */
    --bg: #192330;
    --fg: #cdcecf;
    --snippet: #2b3b51;
    --keyword: #86abdc;
    --fn: #9d79d6;

    /* opt-in light color scheme, taken from dayfox theme */
    @media (prefers-color-scheme: light) {
        --bg: #f6f2ee;
        --fg: #3d2b5a;
        --snippet: #e7d2be;
        --keyword: #287980;
        --fn: #6e33ce;
    }

    background-color: var(--bg);
    color: var(--fg);
    p > code, h2 > code {
        background-color: var(--snippet);
    }
}
a {
    color: var(--keyword);
}
a:visited {
    color: var(--fn);
}

All said and told, we're looking at less than 75 lines of CSS and the site looks presentable.

Finished enough

And for now, we need to put a fork in it and call it done. If you are interested, you can check out the source.

Overall, this took less than a week and was actually pretty enjoyable to work on.

I'm sure there will be other tweaks I'll want to come back and make, but anything else now would probably just be a form of procrastination. What's next is to actually do some writing.