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.
218 lines
5.6 KiB
Markdown
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
|