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:
@@ -0,0 +1,217 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user