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.
123 lines
3.7 KiB
Elixir
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
|