Check how I added a changelog to my Phoenix LiveView web app.
I can add my changelog separately to my blog or docs site. But I decided to experiment with keeping it inside the app.
I’ve also decided to group changes under a version number which doesn’t make much sense for a web app. But I still wanted to do that.
Simplest approach
For a first simple approach I have created a new LiveView named changelog_live.ex:
defmodule OurnextbookWeb.ChangelogLive do
use OurnextbookWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<h1>Changelog</h1>
<h3>v0.2</h3>
<small>2025-02-03</small>
<ul>
<li>Introduce little stars.</li>
<li>Make changelog public.</li>
</ul>
<h3>v0.1</h3>
<small>2024-11-03</small>
<ul>
<li>Add authentication.</li>
<li>Add clubs & memberships.</li>
</ul>
"""
end
endPhoenix LiveViewThis works great, but I always have to type the HTML markup along with the content of the changelog. It’s fine for short entries, but I want to focus on the content.
What if I can use markdown for the changelog content, and have the app convert it dynamically to HTML?
Enter Earmark.
Earmark
Earmark is an Elixir Hex package that parses Markdown and converts it into HTML.
To bring the package in my web app, I am adding the dependency to my mix.exs file:
defp deps do
[
{:earmark, "~> 1.4"}
]
endElixirI then run mix deps.get in my terminal to get the new dependency.
Now I need a new file to put my changelog in markdown format:
# Changelog
### v0.2
<small>2025-02-03</small>
- Introduce little stars.
- Make changelog public.
### v0.1
<small>2024-11-03</small>
- Add authentication.
- Add clubs & memberships.MarkdownI want to replace the static content in changelog_live.ex with the markdown content converted to HTML, dynamically. Here’s the updated file:
defmodule OurnextbookWeb.ChangelogLive do
use OurnextbookWeb, :live_view
alias Earmark
require Logger
def mount(_params, _session, socket) do
changelog_html = load_changelog()
{:ok, assign(socket, changelog_html: changelog_html)}
end
defp load_changelog do
file_path = Application.app_dir(:ournextbook, "priv/static/changelog.md")
case File.read(file_path) do
{:ok, content} ->
Earmark.as_html!(content)
{:error, reason} ->
Logger.error("Failed to load changelog from #{file_path}: #{inspect(reason)}")
"<p>Failed to load changelog.</p>"
end
end
def render(assigns) do
~H"""
<div>
<%= raw(@changelog_html) %>
</div>
"""
end
endPhoenix LiveViewHere’s how it looks:

Thoughts
- I don’t really have to use LiveView for this one. It can be a normal page.
- I generally don’t want to add new dependencies to my apps unless I really have to. Earmark doesn’t bring other dependencies so I am good with that.
- There’s a cool way to watch (in
:dev) the markdown file for changes and refresh the LiveView automatically. But I don’t mind the manual refresh. - Phoenix uses TailwindCSS by default. Normally, I’d have to style the HTML output using the utility classes. But the output looks good without me worrying about figuring out how to style Earmark’s output. How that’s possible you ask? I have introduced a classless approach based on SimpleCSS without removing TailwindCSS. I will blog about that soon. In the meantime, if you need to style the generated HTML, check the Tailwind CSS Typography plugin with its
proseclasses.
Here’s what I am doing
At Amignosis, I pour my heart and skill into crafting slowly brewed software, one thoughtful line at a time. I am craftsman in a world of complexity and low-quality solutions. I am a shoemaker. I take the time to create simple, timeless software built to last. Check what I am doing now and talk to me.
Leave a Reply