RSS Amplifier

Dan Schultzer · Feb 12, 2023

Welcome to my blog

0
Sign in to vote or save

Dan Schultzer · Dan Schultzer

Why go through the effort of building my own blog and not just use ghost?

If you had asked me a few weeks ago I would have agreed that it sounds like a waste of time. I’ve tried so many variations of blogs. From early days with WordPress, to static website generators like jekyll and hexo, to hosted versions like medium. All have their strengths and weaknesses, but I have never found one that suited me perfectly.

Then I read José Valim’s Dashbit blog post on how they rolled their own blog. It appeared there was an effortless way to set up a blog in a Phoenix app with NimblePublisher!

I would have the best of both worlds. It’s kinda static with no database, it’s kinda a publishing system with Phoenix. And it’s all Elixir which is what I do all day. If one day I no longer work with Elixir professionally I would still have a good excuse to keep up with Phoenix development 😄

So that’s what I did over the weekend. Below I’ll detail how I set it up in Phoenix 1.7 using LiveView.

I also recommend reading Elixir School’s lesson and David Bernheisel’s blog post.


First, we’ll generate a new Phoenix app:

mix phx.new my_blog --no-ecto

Add NimblePublisher to the dependencies:

# mix.exs
defmodule MyBlog.MixProject do
  use Mix.Project
  # ...
  def deps do
    [
      # ...
      {:nimble_publisher, "~> 0.1.1"}
    ]
  end
  #...
end

Add the Blog context and Blog.Post module:

# lib/my_blog/blog.ex
defmodule MyBlog.Blog do
  alias MyApp.Blog.Post
  use NimblePublisher,
    build: Post,
    from: Application.app_dir(:my_blog, "priv/posts/**/*.md"),
    as: :posts,
    highlighters: [:makeup_elixir, :makeup_erlang]
  @posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
  def list_posts, do: @posts
  @tags @posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()
  def list_tags, do: @tags
  defmodule NotFoundError do
    defexception [:message, plug_status: 404]
  end
  def get_post_by_id!(id) do
    Enum.find(list_posts(), &(&1.id == id)) ||
      raise NotFoundError, "post with id=#{id} not found"
  end
  def list_posts_by_tag!(tag) do
    case Enum.filter(list_posts(), &(tag in &1.tags)) do
      [] -> raise NotFoundError, "posts with tag=#{tag} not found"
      posts -> posts
    end
  end
end
# lib/my_blog/blog/post.ex
defmodule MyBlog.Blog.Post do
  @enforce_keys [:id, :title, :body, :description, :tags, :date]
  defstruct [:id, :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

Create a new blog post:

# priv/posts/2023/01-01-hello-world.md
%{
  title: "Hello world",
  tags: ~w(hello),
  description: "This is my first blog post"
}
---
Hello world!

Set up the routes:

# lib/my_blog_web/router.ex
defmodule MyBlogWeb.Router do
  use MyBlogWeb, :router
  # ...
  scope "/", MyBlogWeb do
    pipe_through :browser
    live "/", BlogLive, :index
    live "/tags/:tag", BlogLive, :index
    live "/posts/:slug", BlogLive, :show
  end
  # ...
end

Set up the BlogLive module and the templates:

# lib/my_blog_web/live/blog_live.ex
defmodule MyBlogWeb.BlogLive do
  use MyBlogWeb, :live_view
  embed_templates "blog_live/*"
  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end
  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end
  defp apply_action(socket, :index, %{"tag" => tag}) do
    socket
    |> assign(:page_title, tag)
    |> assign(:posts, MyBlog.Blog.list_posts_by_tag!(tag))
    |> assign(:tag, tag)
  end
  defp apply_action(socket, :index, _params) do
    assign(socket, :posts, MyBlog.Blog.list_posts())
  end
  defp apply_action(socket, :show, %{"slug" => slug}) do
    post = MyBlog.Blog.get_post_by_id!(slug)
    socket
    |> assign(:page_title, post.title)
    |> assign(:post, post)
  end
  @impl true
  def render(assigns) do
    apply(__MODULE__, assigns.live_action, [assigns])
  end
end
# lib/my_blog_web/live/blog_live/index.html.eex
<ul>
  <li :for={post <- @posts} class="py-4">
    <article class="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
      <dl>
        <dt class="sr-only">Published on</dt>
        <dd class="text-base font-medium leading-6 text-gray-500">
          <time datetime={post.date}>{post.date}</time>
        </dd>
      </dl>
      <div class="space-y-3 xl:col-span-3">
        <div>
          <h3 class="text-2xl font-bold leading-8 tracking-tight">
            <.link navigate={~p"/posts/#{post.id}"} class="text-gray-900">
              {post.title}
            </.link>
          </h3>
          <ul class="flex flex-wrap">
            <li :for={tag <- post.tags} class="mr-3">
              <.link navigate={~p"/tags/#{tag}"} class="text-sm font-light uppercase text-black/50">
                {tag}
              </.link>
            </li>
          </ul>
        </div>
        <div class="prose max-w-none text-black/50">
          {post.description}
        </div>
      </div>
    </article>
  </li>
</ul>
# lib/my_blog/live/blog_live/show.html.eex
<div class="mx-auto px-4 max-w-4xl">
  <div class="py-5 text-center">
    <p class="text-sm">
      <dt class="sr-only">Published on</dt>
      <dd class="text-base font-medium leading-6 text-white/50">
        <time datetime={@post.date}>{@post.date}</time>
      </dd>
    </p>
    <h1 class="text-5xl font-extrabold">{@post.title}</h1>
  </div>
  <div class="prose py-8 max-w-4xl text-lg">
    {raw @post.body}
  </div>
</div>

Finally, update live reload to improve the dev experience (otherwise you have to restart the server constantly):

config :my_blog, MyBlogWeb.Endpoint,
  live_reload: [
    patterns: [
      # ...
      ~r"posts/*/.*(md)$"
    ]
  ]

Now run mix deps.get, mix phx.server, and visit http://localhost:4000/.

That was easy!

The only thing you are missing is to style it which seems to always be what takes the longest when starting a new blog 😅

Hi, I'm Dan Schultzer, I write in this blog, work a lot in Elixir, maintain several open source projects, and help companies streamline their development process

Read the original on danschultzer.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.