# PhoenixBlog

Plug-and-play blog engine for Phoenix with [Editor.js](https://editorjs.io/) integration.
- Public blog with search, tag filtering, and pagination
- Admin dashboard with Editor.js rich text editor
- Auto-save drafts — posts save automatically as you type
- Embeddable recent posts component for any page
- Self-contained CSS and JS — loads its own assets automatically
- Supports PostgreSQL, MySQL, and SQLite
- Authentication via host app's `on_mount` hooks

## Installation
Add `phoenix_blog` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:phoenix_blog, "~> 0.1"}
]
end
```
## Setup
### 1. Configure the repo
```elixir
# config/config.exs
config :phoenix_blog, repo: MyApp.Repo
```
### 2. Create and run the migration
```bash
mix ecto.gen.migration add_phoenix_blog
```
```elixir
defmodule MyApp.Repo.Migrations.AddPhoenixBlog do
use Ecto.Migration
def up, do: PhoenixBlog.Migration.up()
def down, do: PhoenixBlog.Migration.down()
end
```
```bash
mix ecto.migrate
```
### 3. Serve static assets
Add to your `endpoint.ex`, **before** the existing `Plug.Static`:
```elixir
plug Plug.Static,
at: "/phoenix_blog",
from: {:phoenix_blog, "priv/static"}
```
### 4. Register the JS hooks
In your `assets/js/app.js`:
```javascript
import { PhoenixBlogHooks } from "../../deps/phoenix_blog/priv/static/editorjs/hook.js"
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: { ...PhoenixBlogHooks, ...yourOtherHooks }
})
```
Editor.js scripts are loaded dynamically by the hook — no additional `<script>` tags needed.
### 5. Add Tailwind source
Add the library's templates to your Tailwind source paths so its utility classes get generated. In `assets/css/app.css`:
```css
@source "../../deps/phoenix_blog/lib";
```
### 6. Mount routes
In your `router.ex`:
```elixir
use PhoenixBlog.Web, :router
# Public blog (accessible to everyone)
scope "/" do
pipe_through :browser
phoenix_blog "/blog"
end
# Admin dashboard (protected by your auth)
scope "/" do
pipe_through [:browser, :require_authenticated_user]
phoenix_blog_dashboard "/admin/blog",
on_mount: [{MyAppWeb.UserAuth, :require_authenticated}]
end
```
That's it. The library loads its own CSS (`/phoenix_blog/app.css`) and renders inside your app's root layout automatically.
## Embedding Recent Posts
Show the latest blog posts on any LiveView page using the included component:
```elixir
<.live_component
module={PhoenixBlog.Web.Components.RecentPosts}
id="recent-posts"
blog_path="/blog"
/>
```

| Attribute | Default | Description |
|-----------|---------|-------------|
| `blog_path` | **(required)** | Path where your blog is mounted |
| `count` | `3` | Number of posts to display |
| `title` | `"Latest Posts"` | Section heading (`nil` to hide) |
| `class` | `nil` | Additional CSS classes on the wrapper |
## Configuration
| Option | Default | Description |
|--------|---------|-------------|
| `:repo` | **(required)** | Your Ecto repo module |
| `:table_name` | `"phoenix_blog_posts"` | Database table name |
## Router Options
### `phoenix_blog/2`
| Option | Default | Description |
|--------|---------|-------------|
| `:on_mount` | `[]` | Additional `on_mount` hooks |
| `:as` | `:phoenix_blog` | Live session name |
| `:layout` | `nil` | Custom app layout `{Module, :function}` |
### `phoenix_blog_dashboard/2`
| Option | Default | Description |
|--------|---------|-------------|
| `:on_mount` | `[]` | Authentication hooks (recommended) |
| `:as` | `:phoenix_blog_dashboard` | Live session name |
| `:layout` | `nil` | Custom app layout `{Module, :function}` |
## Features
### Public Blog (`/blog`)
- Responsive card grid with featured images
- Full-text search
- Tag filtering
- Pagination
- SEO-friendly slugs
### Admin Dashboard (`/admin/blog`)
- Post list with search, status filter, and pagination
- Editor.js rich text editor with auto-save
- Draft/Published/Archived status management
- SEO metadata (description, slug, tags)
- Featured image support (via URL)
- Soft delete and restore
### Editor.js Tools
The following tools are included out of the box:
- **Header** (h1-h6)
- **List** (ordered/unordered)
- **Quote** (with caption)
- **Code** (code blocks)
- **Table** (rows/cols)
- **Delimiter** (section break)
- **Embed** (YouTube, Twitter, Vimeo, Instagram, CodePen)
- **Image** (via URL)
## Public API
The `PhoenixBlog` module exposes functions for querying posts from your own code:
```elixir
# Latest published posts (for widgets, feeds, etc.)
PhoenixBlog.list_latest_published_posts(5)
# Paginated published posts with filters
PhoenixBlog.list_published_posts(page: 1, per_page: 10, tag: "elixir")
# Single post by slug
PhoenixBlog.get_post_by_slug!("my-post")
# Admin CRUD
PhoenixBlog.create_post(%{"title" => "Hello", "status" => "draft", ...})
PhoenixBlog.update_post(post, %{"status" => "published"})
PhoenixBlog.publish_post(post)
PhoenixBlog.soft_delete_post(post)
PhoenixBlog.restore_post(post)
```