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
+77
View File
@@ -0,0 +1,77 @@
defmodule FirehoseWeb.BlogLive.Index do
@moduledoc """
Example LiveView for the blog index page.
Copy this into your host app and customize the layout/styling.
Replace `Firehose.EngineeringBlog` with your actual blog module.
"""
use MyAppWeb, :live_view
import Blogex.Components
@impl true
def mount(%{"blog" => blog_id} = _params, _session, socket) do
blog = Blogex.get_blog!(String.to_existing_atom(blog_id))
{:ok,
socket
|> assign(:blog, blog)
|> assign(:page_title, blog.title())}
end
@impl true
def handle_params(params, _uri, socket) do
page = String.to_integer(params["page"] || "1")
tag = params["tag"]
blog = socket.assigns.blog
posts =
if tag do
blog.posts_by_tag(tag)
else
blog.all_posts()
end
per_page = 10
total = length(posts)
total_pages = max(ceil(total / per_page), 1)
entries =
posts
|> Enum.drop((page - 1) * per_page)
|> Enum.take(per_page)
{:noreply,
socket
|> assign(:posts, entries)
|> assign(:tags, blog.all_tags())
|> assign(:current_tag, tag)
|> assign(:page, page)
|> assign(:total_pages, total_pages)
|> assign(:base_path, blog.base_path())}
end
@impl true
def render(assigns) do
~H"""
<div class="max-w-3xl mx-auto py-8 px-4">
<h1 class="text-3xl font-bold mb-2">{@blog.title()}</h1>
<p class="text-gray-600 mb-8">{@blog.description()}</p>
<.tag_list tags={@tags} base_path={@base_path} current_tag={@current_tag} />
<div class="mt-8">
<.post_index posts={@posts} base_path={@base_path} />
</div>
<.pagination page={@page} total_pages={@total_pages} base_path={@base_path} />
<div class="mt-8 text-sm text-gray-500">
<a href={"#{@base_path}/feed.xml"} class="hover:underline">RSS Feed</a>
·
<a href={"#{@base_path}/atom.xml"} class="hover:underline">Atom Feed</a>
</div>
</div>
"""
end
end
+51
View File
@@ -0,0 +1,51 @@
defmodule FirehoseWeb.BlogLive.Show do
@moduledoc """
Example LiveView for showing a single blog post.
Copy this into your host app and customize the layout/styling.
"""
use MyAppWeb, :live_view
import Blogex.Components
@impl true
def mount(%{"blog" => blog_id} = _params, _session, socket) do
blog = Blogex.get_blog!(String.to_existing_atom(blog_id))
{:ok, assign(socket, :blog, blog)}
end
@impl true
def handle_params(%{"slug" => slug}, _uri, socket) do
blog = socket.assigns.blog
post = blog.get_post!(slug)
meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog)
{:noreply,
socket
|> assign(:post, post)
|> assign(:meta, meta)
|> assign(:page_title, post.title)
|> assign(:base_path, blog.base_path())}
end
@impl true
def render(assigns) do
~H"""
<div class="max-w-3xl mx-auto py-8 px-4">
<nav class="mb-8">
<a href={@base_path} class="text-blue-600 hover:underline">
&larr; Back to {@blog.title()}
</a>
</nav>
<.post_show post={@post} />
<footer class="mt-12 pt-8 border-t border-gray-200">
<h3 class="text-lg font-semibold mb-4">Tags</h3>
<.tag_list tags={@post.tags} base_path={@base_path} />
</footer>
</div>
"""
end
end
+89
View File
@@ -0,0 +1,89 @@
defmodule FirehoseWeb.Router do
@moduledoc """
Example router showing how to integrate Blogex.
You have two options for mounting blogs:
## Option A: Plug Router (JSON API / feeds only)
Uses `Blogex.Router` directly — great for headless / API usage:
scope "/blog" do
pipe_through :browser
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
forward "/releases", Blogex.Router, blog: Firehose.ReleaseNotes
end
## Option B: LiveView (full UI, recommended)
Uses your own LiveViews with Blogex components — full control over layout:
scope "/blog", MyAppWeb do
pipe_through :browser
live "/engineering", BlogLive.Index, :index,
metadata: %{blog: :engineering}
live "/engineering/:slug", BlogLive.Show, :show,
metadata: %{blog: :engineering}
live "/releases", BlogLive.Index, :index,
metadata: %{blog: :release_notes}
live "/releases/:slug", BlogLive.Show, :show,
metadata: %{blog: :release_notes}
end
# Still mount the Plug router for feeds
scope "/blog" do
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
forward "/releases", Blogex.Router, blog: Firehose.ReleaseNotes
end
## Option C: Mixed
Use LiveView for the HTML pages but let Blogex.Router handle
feeds and the JSON API. Just make sure the LiveView routes are
defined first so they take priority.
"""
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {FirehoseWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
# -- Option B: LiveView routes (recommended) --
scope "/blog", MyAppWeb do
pipe_through :browser
# Engineering blog
live "/engineering", BlogLive.Index, :index
live "/engineering/tag/:tag", BlogLive.Index, :tag
live "/engineering/:slug", BlogLive.Show, :show
# Release notes
live "/releases", BlogLive.Index, :index
live "/releases/tag/:tag", BlogLive.Index, :tag
live "/releases/:slug", BlogLive.Show, :show
end
# Feeds (served by Blogex.Router as Plug)
scope "/blog" do
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
forward "/releases", Blogex.Router, blog: Firehose.ReleaseNotes
end
# Sitemap
scope "/" do
get "/sitemap.xml", FirehoseWeb.SitemapController, :index
end
end
+14
View File
@@ -0,0 +1,14 @@
defmodule FirehoseWeb.SitemapController do
@moduledoc """
Example controller for serving the blog sitemap.
"""
use MyAppWeb, :controller
def index(conn, _params) do
xml = Blogex.SEO.sitemap(Blogex.blogs(), FirehoseWeb.Endpoint.url())
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, xml)
end
end