# 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
