firehose/blogex/README.md
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

218 lines
5.6 KiB
Markdown

# Blogex
A multi-blog engine for Phoenix apps, powered by [NimblePublisher](https://github.com/dashbitco/nimble_publisher).
Host an engineering blog **and** release notes (or any number of blogs) from markdown files in your repo. Posts compile into the BEAM at build time — zero database, zero runtime I/O, sub-millisecond reads.
## Features
- **Multi-blog support** — run separate blogs from one app (engineering, release notes, etc.)
- **Markdown + frontmatter** — write posts in your editor, version-control in git
- **Compile-time indexing** — NimblePublisher bakes posts into module attributes
- **Tagging & categorization** — filter posts by tag, per-blog or across all blogs
- **RSS & Atom feeds** — auto-generated feeds per blog
- **SEO helpers** — meta tags, OpenGraph, sitemaps
- **Phoenix components** — unstyled function components you wrap in your layout
- **Mountable router** — forward routes to Blogex, it handles the rest
- **Live reload** — edit a `.md` file, see it instantly in dev
## Repository layout
Blogex is designed to live alongside your Phoenix app in a monorepo:
```
firehose/ ← git root
├── app/ ← your Phoenix SaaS (OTP app: :firehose)
│ ├── lib/
│ │ ├── firehose/
│ │ └── firehose_web/
│ ├── priv/
│ │ └── blog/ ← markdown posts live here
│ │ ├── engineering/
│ │ └── release-notes/
│ └── mix.exs
└── blogex/ ← this library
├── lib/
├── test/
└── mix.exs
```
## Installation
In `app/mix.exs`, add blogex as a path dependency:
```elixir
defp deps do
[
{:blogex, path: "../blogex"},
# Optional: syntax highlighting for code blocks
{:makeup_elixir, ">= 0.0.0"},
{:makeup_erlang, ">= 0.0.0"}
]
end
```
## Setup
### 1. Create your posts directory
Inside your Phoenix app:
```
app/priv/blog/
├── engineering/
│ └── 2026/
│ ├── 03-10-our-new-architecture.md
│ └── 02-15-scaling-postgres.md
└── release-notes/
└── 2026/
└── 03-01-v2-launch.md
```
### 2. Write posts with frontmatter
```markdown
%{
title: "Our New Architecture",
author: "Jane Doe",
tags: ~w(elixir otp architecture),
description: "How we rebuilt our platform on OTP"
}
---
## The problem
Our monolith was getting unwieldy...
## The solution
We broke it into an umbrella of focused OTP apps...
```
The filename encodes the date: `YYYY/MM-DD-slug.md`
### 3. Define blog modules
```elixir
# lib/firehose/blogs/engineering_blog.ex
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
# lib/firehose/blogs/release_notes.ex
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
```
### 4. Configure
```elixir
# config/config.exs
config :blogex,
blogs: [Firehose.EngineeringBlog, Firehose.ReleaseNotes]
```
### 5. Mount routes
```elixir
# lib/firehose_web/router.ex
scope "/blog" do
pipe_through :browser
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
forward "/releases", Blogex.Router, blog: Firehose.ReleaseNotes
end
```
### 6. Enable live reload (dev only)
```elixir
# config/dev.exs
live_reload: [
patterns: [
...,
~r"priv/blog/*/.*(md)$"
]
]
```
## Usage
### Querying a single blog
```elixir
Firehose.EngineeringBlog.all_posts()
Firehose.EngineeringBlog.recent_posts(5)
Firehose.EngineeringBlog.get_post!("our-new-architecture")
Firehose.EngineeringBlog.posts_by_tag("elixir")
Firehose.EngineeringBlog.all_tags()
Firehose.EngineeringBlog.paginate(1, 10)
```
### Querying across all blogs
```elixir
Blogex.all_posts() # all posts from all blogs, newest first
Blogex.all_tags() # all unique tags across blogs
Blogex.get_blog!(:engineering) # get the blog module
```
### Using Phoenix components
```heex
import Blogex.Components
<.post_index posts={@posts} base_path="/blog/engineering" />
<.post_show post={@post} />
<.tag_list tags={@tags} base_path="/blog/engineering" current_tag={@tag} />
<.pagination page={@page} total_pages={@total_pages} base_path="/blog/engineering" />
```
### Generating feeds
The mounted router serves feeds automatically at `/feed.xml` and `/atom.xml`.
You can also generate them manually:
```elixir
Blogex.Feed.rss(Firehose.EngineeringBlog, "https://firehose.dev")
Blogex.Feed.atom(Firehose.EngineeringBlog, "https://firehose.dev")
```
### SEO
```elixir
# Meta tags for a post
meta = Blogex.SEO.meta_tags(post, "https://firehose.dev", Firehose.EngineeringBlog)
# Sitemap across all blogs
xml = Blogex.SEO.sitemap(Blogex.blogs(), "https://firehose.dev")
```
## Architecture
Blogex follows the **poncho pattern** — it wraps NimblePublisher and presents
a clean API to your Phoenix app. There are no GenServers or processes; all post
data is compiled into BEAM bytecode via module attributes. This means:
- Reads are instant (no I/O, no database)
- Posts update on recompilation (or live reload in dev)
- The host app owns the layout, styling, and routing
- Blogex is a sibling library, not an umbrella child — it has its own `mix.exs` and test suite
- When ready to open-source or publish to Hex, swap `path: "../blogex"` for a version number
## License
MIT