Packages

A structured content layer for Phoenix LiveView apps: YAML and Markdown files, validated, loaded, and served.

Current section

Files

Jump to
brix usage-rules.md
Raw

usage-rules.md

# Brix Usage Rules
Brix is a flat-file CMS library for Phoenix. It reads YAML and Markdown content from `priv/content/`, validates and caches it in ETS, and provides a structured API for rendering pages in LiveView.
## Core API
### Fetching Resources
All `get_*` functions return `{:ok, struct}` or `:error`. All `list_*` functions return a list.
```elixir
# Pages
{:ok, page} = Brix.get_page("/blog/hello")
pages = Brix.list_pages()
# Other resources
{:ok, layout} = Brix.get_layout("default")
{:ok, author} = Brix.get_author("maya")
{:ok, tag} = Brix.get_tag("coffee")
{:ok, media} = Brix.get_media("hero-image")
{:ok, section} = Brix.get_shared_section("main-nav")
{:ok, collection} = Brix.get_collection("blog")
{:ok, tmpl} = Brix.get_section_template("hero")
```
### Page Filters
`Brix.list_pages/1` accepts keyword opts. All filters compose.
```elixir
Brix.list_pages(
tag: "coffee", # pages with this tag slug
author: "maya", # pages with this author slug
prefix: "/blog/", # pages whose slug starts with prefix
status: :published # :published (default) | :draft | :all
)
```
### Page Helpers
```elixir
Brix.Page.published?(page) # true if published_at <= now
Brix.Page.matches?(page, "query") # case-insensitive search
Brix.Page.excerpt(page) # plain-text excerpt, 200 chars
Brix.Page.excerpt(page, 100) # custom length
```
### Collections
Collections define reusable page queries with filters and sort order.
```elixir
{:ok, collection} = Brix.get_collection("blog")
pages = Brix.list_collection_pages(collection)
```
### Slug Redirects
Pages can declare old slugs in `slug_history`. Look them up with:
```elixir
case Brix.find_redirect("/old-url") do
{:ok, new_slug} -> push_navigate(socket, to: new_slug)
:error -> raise NotFoundError
end
```
### Reloading Content
```elixir
Brix.reload() # reloads all content from disk
```
The store is a GenServer (`Brix.Store.Filesystem`). After editing content files on disk, call `Brix.reload/0` to refresh the ETS cache.
## Section Fields Are String-Keyed
Section field maps use **string keys**, not atom keys. This is the most common mistake.
```elixir
# Correct
@fields["heading"]
@fields["body"]
# Wrong - will always be nil
@fields[:heading]
@fields.heading
```
## Richtext Fields Contain HTML
Richtext fields are converted from Markdown to HTML at load time. Use `raw/1` in templates.
```elixir
# Correct
{raw(@fields["body"])}
# Wrong - will render escaped HTML tags
{@fields["body"]}
```
## Use source_fields for Raw Markdown
Each `%Brix.Section{}` has both `fields` (HTML) and `source_fields` (raw Markdown). When you need the original Markdown (for LLM endpoints, feeds, etc.), use `source_fields`.
```elixir
# Correct - raw markdown preserved from load time
section.source_fields["body"]
# Wrong - regex-stripping HTML from the rendered output
section.fields["body"] |> String.replace(~r/<[^>]+>/, "")
```
## Rendering in LiveView
### Section Components
Define a module with function components. Each function name **must match** the section template name exactly.
```elixir
defmodule MyAppWeb.Sections do
use Phoenix.Component
# Template name "hero" -> function name hero/1
def hero(assigns) do
~H"""
<section>
<h1>{@fields["heading"]}</h1>
</section>
"""
end
# Template name "article_body" -> function name article_body/1
def article_body(assigns) do
~H"""
<article class="prose">
{raw(@fields["body"])}
</article>
"""
end
end
```
### Dispatching Sections and Layouts
`Brix.Render.sections/1` dispatches each section to the matching function component. `Brix.Render.layout/1` renders header sections, inner block, then footer sections.
```heex
<Brix.Render.layout layout={@layout} module={MyAppWeb.Sections}>
<Brix.Render.sections sections={@page.sections} module={MyAppWeb.Sections} />
</Brix.Render.layout>
```
### Media URLs
Resolve media slugs to serving paths with `Brix.Render.media_url/1`. Do not construct paths manually.
```elixir
# Correct
Brix.Render.media_url("hero-image") #=> "/content/media/files/hero.jpg"
# Wrong
"/content/media/#{media.path}"
```
## SEO Metadata
Use `Brix.Meta.field/3` for SEO fields. It resolves with a page-then-site fallback chain. Do not access `page.meta_title` or `site.meta_title` directly.
```elixir
site = Brix.get_site()
{:ok, page} = Brix.get_page("/about")
Brix.Meta.field(page, site, :title) # page.meta_title || page.title || site.meta_title || site.name
Brix.Meta.field(page, site, :description) # page.meta_description || site.meta_description
Brix.Meta.field(page, site, :og_title) # page.og_title || :title fallback
Brix.Meta.field(page, site, :og_description) # page.og_description || :description fallback
Brix.Meta.field(page, site, :og_image) # page.og_image || site.og_image
```
Works with both `%Page{}` and `%Collection{}` structs.
## Store Setup
Add `Brix.Store.Filesystem` to your supervision tree:
```elixir
# application.ex
children = [
{Brix.Store.Filesystem,
content_dir: Path.join(:code.priv_dir(:my_app), "content")},
# ...
]
```
Content is validated on boot. Validation errors block loading.
## Content Directory Structure
```
priv/content/
├── site.yml # Site config (required)
├── authors/{slug}.yml # Author definitions
├── tags/{slug}.yml # Tag definitions
├── media/{slug}.yml # Media metadata
│ └── files/ # Actual media files
├── templates/sections/{name}.yml # Section field schemas
├── layouts/{name}.yml # Header/footer section lists
├── shared_sections/{name}.yml # Reusable section blocks
├── collections/{slug}.yml # Page groupings with filters
└── pages/{slug}/
├── page.yml # Page metadata
└── versions/{YYYYMMDDTHHMMSSz}/
├── version.yml # published_at, updated_at
└── sections/
├── 01-{name}.yml # YAML section
├── 02-{name}.md # Standalone markdown section
├── 03-{name}.{field}.md # Mixed: markdown field merged into YAML
└── 02-{name}.{field}/ # Nested child sections directory
└── 01-{child}.yml
```
### Section File Formats
**YAML** (`01-hero.yml`): Template and fields defined in YAML.
**Standalone Markdown** (`02-intro.md`): Template from frontmatter. Entire body becomes the content field.
**Mixed** (`03-cta.yml` + `03-cta.body.md`): YAML defines most fields. The `.body.md` file's content merges into the `body` field. The raw markdown is preserved in `source_fields`.
### Section Template Field Types
| Type | Description |
| ---------- | -------------------------------------- |
| `string` | Plain text |
| `richtext` | Markdown converted to HTML at load |
| `media` | Media slug reference |
| `url` | URL string |
| `boolean` | `true` / `false` |
| `integer` | Numeric value |
| `list` | List of items (optional `of:` subtype) |
| `map` | Key-value object |
| `sections` | Nested child sections (optional `of:`) |
### Nested Sections
Sections can contain child sections via the `sections` field type. Children live in subdirectories named `{NN}-{template}.{field}/` alongside their parent file:
```
sections/
├── 01-hero.yml
├── 02-gallery.yml
├── 02-gallery.slides/ # children for the "slides" field
│ ├── 01-slide.yml
│ ├── 01-slide.caption.md # mixed markdown works in children too
│ ├── 02-slide.yml
│ └── 03-slide.yml
└── 03-cta.yml
```
Declare which fields accept children in the section template:
```yaml
# templates/sections/gallery.yml
fields:
title:
type: string
required: true
slides:
type: sections
of: slide # constrains children to "slide" template
required: true
```
`of` supports a single template name or a list. Omitting `of` allows any template.
### Rendering Nested Sections
Child sections are available in `@children` (a `%{field_name => [Section]}` map) and `@module` is passed through. Use `Brix.Render.child_sections/1` to render them:
```elixir
def gallery(assigns) do
~H"""
<div class="gallery">
<h2>{@fields["title"]}</h2>
<Brix.Render.child_sections module={@module} children={@children} field="slides" />
</div>
"""
end
```
Existing components that only use `@fields` are unaffected — `@children` defaults to `%{}`.
### Page Publishing
| `published_at` value | Status |
| -------------------- | --------- |
| absent / `nil` | Draft |
| Future datetime | Scheduled |
| Past datetime | Published |
### Layouts
Layouts reference sections inline or via shared section refs. Shared refs are resolved at load time — the `%Layout{}` struct always contains fully resolved `%Section{}` structs.
```yaml
# layouts/default.yml
header_sections:
- shared_section: main-nav # resolved from shared_sections/main-nav.yml
footer_sections:
- template: footer # inline section definition
fields:
copyright: "2025 My Site"
```
## Common Mistakes
- **Atom keys on fields**: `@fields[:heading]` is always `nil`. Use `@fields["heading"]`.
- **Missing `raw/1`**: Richtext fields contain HTML. Without `raw/1`, tags render as escaped text.
- **Stripping HTML for markdown**: Use `source_fields`, not regex on `fields`.
- **Direct meta access**: Use `Brix.Meta.field/3` instead of `page.meta_title` to get the full fallback chain.
- **Wrong component name**: If the template is `"hero_home"`, the function must be `hero_home/1`, not `hero/1`.
- **Manual media paths**: Use `Brix.Render.media_url/1` to resolve slugs.
- **Forgetting reload**: After editing content files, call `Brix.reload/0` to refresh the cache.