Your Name bc14696f57 Static blog with front page summary
Goal: have a personal blog, and try out another point in the 'modular
app design with elixir' space.

Designing OTP systems with elixir had some interesting ideas.
2026-03-17 11:17:21 +00:00

123 lines
3.7 KiB
Elixir

defmodule Blogex.Blog do
@moduledoc """
Macro to define a blog context backed by NimblePublisher.
## Usage
In your host application, define one module per blog:
defmodule Firehose.EngineeringBlog do
use Blogex.Blog,
blog_id: :engineering,
app: :firehose,
from: "priv/blog/engineering/**/*.md",
title: "Engineering Blog",
description: "Deep dives into our tech stack",
base_path: "/blog/engineering"
end
defmodule Firehose.ReleaseNotes do
use Blogex.Blog,
blog_id: :release_notes,
app: :firehose,
from: "priv/blog/release-notes/**/*.md",
title: "Release Notes",
description: "What's new in Firehose",
base_path: "/blog/releases"
end
Each module compiles all markdown posts at build time and exposes
query functions like `all_posts/0`, `get_post!/1`, `posts_by_tag/1`, etc.
"""
defmacro __using__(opts) do
blog_id = Keyword.fetch!(opts, :blog_id)
app = Keyword.fetch!(opts, :app)
from = Keyword.fetch!(opts, :from)
title = Keyword.fetch!(opts, :title)
description = Keyword.get(opts, :description, "")
base_path = Keyword.fetch!(opts, :base_path)
highlighters = Keyword.get(opts, :highlighters, [:makeup_elixir, :makeup_erlang])
quote do
alias Blogex.Post
use NimblePublisher,
build: Post,
from: Application.app_dir(unquote(app), unquote(from)),
as: :posts,
highlighters: unquote(highlighters)
# Inject the blog_id into each post and sort by descending date
@posts @posts
|> Enum.map(&Map.put(&1, :blog, unquote(blog_id)))
|> Enum.sort_by(& &1.date, {:desc, Date})
# Collect all unique tags
@tags @posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()
@blog_id unquote(blog_id)
@blog_title unquote(title)
@blog_description unquote(description)
@blog_base_path unquote(base_path)
@doc "Returns the blog identifier atom."
def blog_id, do: @blog_id
@doc "Returns the blog title."
def title, do: @blog_title
@doc "Returns the blog description."
def description, do: @blog_description
@doc "Returns the base URL path for this blog."
def base_path, do: @blog_base_path
@doc "Returns all published posts, newest first."
def all_posts, do: Enum.filter(@posts, & &1.published)
@doc "Returns the N most recent published posts."
def recent_posts(n \\ 5), do: Enum.take(all_posts(), n)
@doc "Returns all unique tags across all published posts."
def all_tags, do: @tags
@doc "Returns all published posts matching the given tag."
def posts_by_tag(tag) do
Enum.filter(all_posts(), fn post -> tag in post.tags end)
end
@doc "Returns a single post by slug/id, or raises."
def get_post!(id) do
Enum.find(all_posts(), &(&1.id == id)) ||
raise Blogex.NotFoundError, "post #{inspect(id)} not found in #{@blog_id}"
end
@doc "Returns a single post by slug/id, or nil."
def get_post(id) do
Enum.find(all_posts(), &(&1.id == id))
end
@doc "Returns paginated posts. Page is 1-indexed."
def paginate(page \\ 1, per_page \\ 10) do
posts = all_posts()
total = length(posts)
total_pages = max(ceil(total / per_page), 1)
entries =
posts
|> Enum.drop((page - 1) * per_page)
|> Enum.take(per_page)
%{
entries: entries,
page: page,
per_page: per_page,
total_entries: total,
total_pages: total_pages
}
end
end
end
end