# PhoenixPageMeta
[](https://hex.pm/packages/phoenix_page_meta)
[](https://hexdocs.pm/phoenix_page_meta)
[](https://github.com/saschabrink/phoenix_page_meta/actions/workflows/ci.yml)
[](https://github.com/saschabrink/phoenix_page_meta/blob/main/LICENSE)
Per-page metadata for Phoenix LiveView apps: breadcrumbs, active-link state, SEO meta tag rendering. One struct per page, one render in the layout, no per-project re-implementation of the same logic in three different places.
## Why
Every Phoenix app I write ends up with the same trio:
* breadcrumb logic somewhere,
* active-link helpers duplicated across sidebar/nav/layout components
* a layout's worth of SEO meta tags hand-built per project.
PhoenixPageMeta standardises all three: each LiveView declares a `%PageMeta{}` struct for the current page, and the library renders the SEO meta tags, builds the breadcrumb trail, and resolves active-link state from it. Site-wide values and any custom fields are set in `config :phoenix_page_meta`.
## Installation
```elixir
def deps do
[{:phoenix_page_meta, "~> 0.2"}]
end
```
With [Igniter](https://hex.pm/packages/igniter), the installer adds the config, the root.html.heex meta tags, and the LiveView wiring automatically:
```sh
mix igniter.install phoenix_page_meta
```
## Setup
Each page is described by a `%PhoenixPageMeta.PageMeta{}`. The base URL is
auto-detected from your Phoenix endpoint, so it works with no configuration:
```elixir
%PhoenixPageMeta.PageMeta{title: "Hello", path: "/hello"}
```
### Configuration
All config is optional — add it to set site-wide defaults or extend the struct:
```elixir
# config/config.exs
config :phoenix_page_meta,
site_name: "MyApp",
twitter_site: "@myapp",
supported_locales: [:en, :es],
extra_fields: [icon: nil, modal: false]
```
- Site-wide values (`site_name`, `twitter_site`, `supported_locales`,
`og_type`) become struct defaults; a page may override any of them.
- `extra_fields` adds custom fields (bare atoms and `{key, default}` pairs may
be mixed). Make some required with `extra_enforce_keys: [...]`.
- `base_url` is auto-detected from the single running `Phoenix.Endpoint`. Set
`base_url:` (string, `&Mod.url/0`, or `{m, f, a}`) or `endpoint:
MyAppWeb.Endpoint` only if detection is ambiguous (e.g. multiple endpoints).
> Field-set/default config is read with `Application.compile_env/3`. After
> changing `:extra_fields`, `:extra_enforce_keys`, or the site-wide defaults, run
> `mix deps.compile phoenix_page_meta --force`.
### Wire LiveView
In `MyAppWeb.live_view/0`:
```elixir
def live_view do
quote do
use Phoenix.LiveView, layout: {MyAppWeb.Layouts, :app}
@behaviour PhoenixPageMeta.LiveView
import PhoenixPageMeta.LiveView, only: [assign_page_meta: 1]
end
end
```
Add `alias PhoenixPageMeta.PageMeta` inside your `html_helpers` so templates and
LiveViews can write `%PageMeta{}` unprefixed (the examples below assume this).
## Usage
In each LiveView, implement `page_meta/2` and call `assign_page_meta/1` after data is loaded:
```elixir
defmodule MyAppWeb.LocationLive.Show do
use MyAppWeb, :live_view
# assumes `alias PhoenixPageMeta.PageMeta` in your MyAppWeb html_helpers
@impl PhoenixPageMeta.LiveView
def page_meta(socket, :show) do
location = socket.assigns.location
%PageMeta{
title: location.name,
path: ~p"/locations/#{location.slug}",
description: location.summary,
parent: %PageMeta{title: "Locations", path: ~p"/locations"}
}
end
def handle_params(params, _uri, socket) do
{:noreply,
socket
|> assign(:location, load_location(params))
|> assign_page_meta()}
end
end
```
In your root layout, render the meta tags:
```heex
<head>
<PhoenixPageMeta.Components.MetaTags.default page_meta={@page_meta} />
<.live_title>{@page_title}</.live_title>
</head>
```
In nav components, use `PageMeta.active?/2`:
```heex
<.link navigate={~p"/locations"} class={PageMeta.active?(@page_meta, ~p"/locations") && "active"}>
Locations
</.link>
```
For breadcrumbs, use the slot-based component (handles `aria-label`, `aria-current`, divider placement):
```heex
<PhoenixPageMeta.Components.Breadcrumbs.list page_meta={@page_meta}>
<:link :let={breadcrumb}>
<.link navigate={breadcrumb.path} class="hover:underline truncate">
<.icon :if={breadcrumb.page_meta.icon} name={breadcrumb.page_meta.icon} class="size-4" />
{breadcrumb.title}
</.link>
</:link>
<:current :let={breadcrumb}>
<span class="font-medium truncate">{breadcrumb.title}</span>
</:current>
<:divider>
<span class="text-base-content/30">/</span>
</:divider>
</PhoenixPageMeta.Components.Breadcrumbs.list>
```
(`breadcrumb.page_meta.icon` assumes you added `icon` via `extra_fields`.)
## Standard fields
| Field | Type | Notes |
|---|---|---|
| `:title` | `String.t()` | required |
| `:path` | `String.t()` | required |
| `:breadcrumb_title` | `String.t() \| nil` | falls back to `:title` in breadcrumbs |
| `:parent` | `t() \| nil` | parent page; walked for breadcrumbs |
| `:description` | `String.t() \| nil` | meta description, og:description, twitter:description |
| `:og_image` | `String.t() \| nil` | OG and Twitter image. Relative paths are auto-prefixed with `base_url`; absolute URLs (`http://`/`https://`) are rendered as-is |
| `:og_image_alt` | `String.t() \| nil` | alt text for og:image; falls back to `:title` |
| `:og_type` | `String.t()` | default from `config :phoenix_page_meta, :og_type` (else `"website"`) |
| `:json_ld` | `map() \| nil` | rendered as `<script type="application/ld+json">` |
| `:canonical_path` | `String.t() \| nil` | overrides `:path` for canonical URL |
| `:noindex` | `boolean()` | default `false` |
| `:locale` | `atom() \| nil` | current page's locale; renders `og:locale` |
| `:supported_locales` | `[atom()] \| nil` | hreflang tags + `og:locale:alternate`; default from config |
| `:site_name` | `String.t() \| nil` | renders `og:site_name`; default from config |
| `:twitter_site` | `String.t() \| nil` | renders `twitter:site` (e.g. `"@handle"`); default from config |
| `:skip_breadcrumb` | `boolean() \| nil` | when `true`, this page is filtered out of the breadcrumb (modals, overlays) |
Add app-specific fields via `config :phoenix_page_meta, extra_fields: [...]` (e.g. `:icon`, `:modal`).
## Migrating from `use PhoenixPageMeta`
Before 0.2.0 you declared a per-app `MyAppWeb.PageMeta` module with `use
PhoenixPageMeta` and an explicit `defstruct`. That path is **deprecated** (it
still works but emits a compile-time warning) and will be removed in a future
release. To migrate:
1. Move site-wide struct defaults into `config :phoenix_page_meta`, and any
custom fields into `extra_fields`.
2. Replace `%MyAppWeb.PageMeta{}` with `%PhoenixPageMeta.PageMeta{}` (or just
re-point your `alias`).
3. Delete the `MyAppWeb.PageMeta` module.
The only capability the `use` macro offers that the prebuilt struct does not is
multiple distinct PageMeta modules (e.g. an umbrella with several endpoints).
## License
MIT