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.
This commit is contained in:
Your Name
2026-03-17 11:17:21 +00:00
commit bc14696f57
94 changed files with 6846 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
defmodule Blogex.Test.FakeBlog do
@moduledoc """
A test double that implements the same interface as a `use Blogex.Blog`
module, but backed by an Agent so tests can control the post data.
## Usage in tests
setup do
posts = [PostBuilder.build(id: "hello"), PostBuilder.build(id: "world")]
Blogex.Test.FakeBlog.start(posts, blog_id: :engineering, title: "Eng Blog")
:ok
end
Then pass `Blogex.Test.FakeBlog` anywhere a blog module is expected.
"""
use Agent
@defaults [
blog_id: :test_blog,
title: "Test Blog",
description: "A blog for tests",
base_path: "/blog/test"
]
def start(posts \\ [], opts \\ []) do
opts = Keyword.merge(@defaults, opts)
state = %{
posts: posts,
blog_id: opts[:blog_id],
title: opts[:title],
description: opts[:description],
base_path: opts[:base_path]
}
case Agent.start(fn -> state end, name: __MODULE__) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} ->
Agent.update(__MODULE__, fn _ -> state end)
{:ok, pid}
end
end
def stop, do: Agent.stop(__MODULE__)
defp get(key), do: Agent.get(__MODULE__, &Map.fetch!(&1, key))
def blog_id, do: get(:blog_id)
def title, do: get(:title)
def description, do: get(:description)
def base_path, do: get(:base_path)
def all_posts do
get(:posts)
|> Enum.filter(& &1.published)
|> Enum.sort_by(& &1.date, {:desc, Date})
end
def recent_posts(n \\ 5), do: Enum.take(all_posts(), n)
def all_tags do
all_posts()
|> Enum.flat_map(& &1.tags)
|> Enum.uniq()
|> Enum.sort()
end
def posts_by_tag(tag) do
Enum.filter(all_posts(), fn post -> tag in post.tags end)
end
def get_post!(id) do
Enum.find(all_posts(), &(&1.id == id)) ||
raise Blogex.NotFoundError, "post #{inspect(id)} not found"
end
def get_post(id) do
Enum.find(all_posts(), &(&1.id == id))
end
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
+48
View File
@@ -0,0 +1,48 @@
defmodule Blogex.Test.PostBuilder do
@moduledoc """
Builds `%Blogex.Post{}` structs for tests.
Call `build/0` for a post with sensible defaults, or `build/1`
with a keyword list to override only the fields that matter for
your specific test.
build() # generic post
build(title: "Specific Title") # override one field
build(tags: ~w(elixir otp), blog: :eng) # override several
build(published: false) # draft post
"""
@defaults %{
id: "a-blog-post",
title: "A Blog Post",
author: "Test Author",
body: "<p>Post body.</p>",
description: "A test post",
date: ~D[2026-01-15],
tags: ["general"],
blog: :test_blog,
published: true
}
@doc "Build a post with defaults, merging any overrides."
def build(overrides \\ []) do
attrs = Map.merge(@defaults, Map.new(overrides))
struct!(Blogex.Post, attrs)
end
@doc "Build a list of n posts with sequential dates (newest first)."
def build_many(n, overrides \\ []) do
Enum.map(1..n, fn i ->
build(
Keyword.merge(
[
id: "post-#{i}",
title: "Post #{i}",
date: Date.add(~D[2026-01-01], n - i)
],
overrides
)
)
end)
end
end
+50
View File
@@ -0,0 +1,50 @@
defmodule Blogex.Test.Setup do
@moduledoc """
Reusable setup blocks for blog tests.
"""
import Blogex.Test.PostBuilder
@doc """
Starts a FakeBlog with a standard set of posts.
Returns `%{blog: module, posts: posts}`.
"""
def with_blog(context \\ %{}, opts \\ []) do
posts = Keyword.get(opts, :posts, default_posts())
blog_opts = Keyword.drop(opts, [:posts])
{:ok, _} = Blogex.Test.FakeBlog.start(posts, blog_opts)
Map.merge(context, %{blog: Blogex.Test.FakeBlog, posts: posts})
end
@doc "A small set of posts covering common scenarios."
def default_posts do
[
build(
id: "newest-post",
date: ~D[2026-03-10],
tags: ["elixir", "otp"],
published: true
),
build(
id: "middle-post",
date: ~D[2026-02-15],
tags: ["elixir", "testing"],
published: true
),
build(
id: "oldest-post",
date: ~D[2026-01-05],
tags: ["devops"],
published: true
),
build(
id: "draft-post",
date: ~D[2026-03-12],
tags: ["elixir"],
published: false
)
]
end
end