Current section
Files
Jump to
Current section
Files
phoenix_page_meta
usage-rules.md
usage-rules.md
# phoenix_page_meta usage rules
Rules apply to `phoenix_page_meta ~> 0.2`.
Per-page metadata for Phoenix LiveView apps: breadcrumbs, active-link state,
SEO meta tag rendering. Each LiveView declares one `%PhoenixPageMeta.PageMeta{}`
per action; the package handles the rest.
## The shape
As of 0.2.0 you use the prebuilt, config-driven `PhoenixPageMeta.PageMeta`
struct directly — there is **no per-app PageMeta module**.
1. **`config/config.exs`** — site-wide defaults and (optionally) extra fields.
2. **Each LiveView** — implements `PhoenixPageMeta.LiveView` callback
`page_meta/2`, calls `assign_page_meta/1` after data is loaded.
```elixir
# config/config.exs
config :phoenix_page_meta,
site_name: "MyApp",
twitter_site: "@myapp",
supported_locales: [:en, :es],
extra_fields: [icon: nil, modal: false]
```
```elixir
# in a LiveView
%PhoenixPageMeta.PageMeta{title: "Hello", path: "/hello"}
```
> The legacy `use PhoenixPageMeta` + hand-rolled `defstruct` approach is
> **deprecated** as of 0.2.0 (emits a compile-time warning). Prefer the
> prebuilt struct. The only thing the legacy path still offers is multiple
> distinct PageMeta modules (e.g. an umbrella with several endpoints).
## The struct
`PhoenixPageMeta.PageMeta`'s field set is assembled at compile time:
- **Base fields** (always present): `:title`, `:path`, `:breadcrumb_title`,
`:parent`, `:skip_breadcrumb`, `:description`, `:canonical_path`,
`:og_image`, `:og_image_alt`, `:json_ld`, `:locale`, plus `:noindex`
(default `false`).
- **Config-defaulted fields**: `:og_type`, `:site_name`, `:twitter_site`,
`:supported_locales` — their defaults come from `config :phoenix_page_meta`
(so they are set site-wide, overridable per page).
- **Extra fields**: from `config :phoenix_page_meta, extra_fields: [...]`. Bare
atoms and `{key, default}` pairs may be mixed; a later entry overrides an
earlier one.
`:title` and `:path` are required (`@enforce_keys`). Make extra fields required
with `config :phoenix_page_meta, extra_enforce_keys: [...]` (appended to
`:title`/`:path`).
> **Compile-time config.** `:extra_fields`, `:extra_enforce_keys`, `:og_type`,
> `:site_name`, `:twitter_site`, and `:supported_locales` are read with
> `Application.compile_env/3`. After changing them run
> `mix deps.compile phoenix_page_meta --force`.
## base_url and lang_path
These are resolved from config, not from the struct module:
- **`base_url`** — `config :phoenix_page_meta, :base_url` (a string,
`&Mod.url/0`, or `{m, f, a}`). If unset, the single running
`Phoenix.Endpoint` is auto-detected (memoized). Set `:endpoint`
(`config :phoenix_page_meta, endpoint: MyAppWeb.Endpoint`) or `:base_url`
explicitly if detection is ambiguous (e.g. multiple endpoints).
- **`lang_path`** — `config :phoenix_page_meta, :lang_path` (a `fun/2` or
`{m, f}`). Defaults to a locale-prefix swap (`/en/foo` → `/de/foo`).
In the common single-endpoint app, no base-URL config is needed at all.
## Module structure
```
PhoenixPageMeta # active?/2,3 (lib-level), config resolvers
PhoenixPageMeta.PageMeta # prebuilt config-driven struct (recommended)
PhoenixPageMeta.Breadcrumb # struct + build/1
PhoenixPageMeta.Components.Breadcrumbs # list/1 component
PhoenixPageMeta.Components.MetaTags # default/1 component
PhoenixPageMeta.Site # behaviour: base_url/0, lang_path/2
PhoenixPageMeta.LiveView # behaviour: page_meta/2 + assign_page_meta/1
```
## Standard fields
URL fields (`og_image`, `canonical_path`, `path`) accept either absolute URLs
(`https://...`) or relative paths (`/foo`). Relative paths are auto-prefixed
with `base_url` at render time. So `og_image: "/images/og.png"` produces
`<meta property="og:image" content="https://example.com/images/og.png" />`.
`:site_name`, `:twitter_site`, and `:supported_locales` are typically set once
via config (site-wide) and rarely overridden per page.
`MetaTags.default` reads optional fields with `Map.get/2`, so a struct compiled
without a given extra field still renders.
## LiveView pattern
```elixir
# In MyAppWeb.live_view/0:
quote do
use Phoenix.LiveView, layout: ...
@behaviour PhoenixPageMeta.LiveView
import PhoenixPageMeta.LiveView, only: [assign_page_meta: 1]
end
# In each LiveView:
@impl PhoenixPageMeta.LiveView
def page_meta(socket, :show) do
%PhoenixPageMeta.PageMeta{
title: socket.assigns.location.name,
path: ~p"/locations/#{socket.assigns.location.slug}",
parent: %PhoenixPageMeta.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
```
Alias `PhoenixPageMeta.PageMeta` in your web module's `html_helpers` so
templates can write `%PageMeta{}` unprefixed.
## active?/2,3
```elixir
PhoenixPageMeta.PageMeta.active?(page_meta, "/locations") # prefix match, query stripped
PhoenixPageMeta.PageMeta.active?(page_meta, "/locations", exact: true) # exact match only
PhoenixPageMeta.PageMeta.active?(page_meta, "/x?tab=open", query: true) # raw match (filter tabs)
```
The wrapper guards on `is_struct(page_meta, PhoenixPageMeta.PageMeta)` — wrong
type = `FunctionClauseError`.
Without `:exact`, a path is active if it equals the current path or is a prefix
followed by `/`. `/locations` is active on `/locations/123` but not on
`/location-foo`.
## breadcrumbs
`PhoenixPageMeta.PageMeta.breadcrumbs/1` (delegates to
`PhoenixPageMeta.Breadcrumb.build/1`) walks the `:parent` chain and returns
root-first `[%PhoenixPageMeta.Breadcrumb{title, path, first?, last?, page_meta}]`.
Pages with `:skip_breadcrumb: true` are filtered out — useful for modal or
overlay routes that should not appear as breadcrumb entries.
## Layout
```heex
<head>
<PhoenixPageMeta.Components.MetaTags.default page_meta={@page_meta} />
<.live_title suffix=" | MyApp">{@page_title}</.live_title>
</head>
```
## Breadcrumbs component
Slot-based, owns `<nav aria-label>`, `aria-current="page"`, divider placement.
Accepts `:rest` globals on `<nav>` (e.g. `class`). Renders nothing when the
trail is empty.
```heex
<PhoenixPageMeta.Components.Breadcrumbs.list page_meta={@page_meta}>
<:link :let={breadcrumb}>
<.link navigate={breadcrumb.path}>
<.icon :if={breadcrumb.page_meta.icon} name={breadcrumb.page_meta.icon} />
{breadcrumb.title}
</.link>
</:link>
<:current :let={breadcrumb}>
<span>{breadcrumb.title}</span>
</:current>
<:divider>/</:divider>
</PhoenixPageMeta.Components.Breadcrumbs.list>
```
(`breadcrumb.page_meta.icon` assumes you added `icon` via `extra_fields`.)
## Do
- **Always set `:path`.** Required, used by canonical, og:url, hreflang,
active-link matching.
- **Use `:parent` for hierarchy**, not URL prefix tricks. The parent chain is
the source of truth for breadcrumbs.
- **Call `assign_page_meta/1` last** in mount/handle_params, after all assigns
the `page_meta/2` callback reads are set.
- **Put site-wide values in config** (`site_name`, `twitter_site`,
`supported_locales`), not on every page struct.
- **Add app-specific fields via `extra_fields`** (e.g. `:icon`, `:modal`).
- **Recompile after config changes** to compile-time keys:
`mix deps.compile phoenix_page_meta --force`.
- **Set `:canonical_path`** when the same content lives at multiple paths.
- **Set `:skip_breadcrumb: true`** on modal/overlay routes.
## Don't
- **Don't use `use PhoenixPageMeta` + `defstruct`.** Deprecated — use the
prebuilt `PhoenixPageMeta.PageMeta` struct.
- **Don't read extra fields as if guaranteed.** Base fields always exist;
`extra_fields` are config-dependent — guard with `Map.get/2` in app code.
- **Don't render breadcrumbs by hand if you can use the component.** It owns
the a11y bits (aria-label, aria-current) and divider placement.
- **Don't set `:supported_locales` without ensuring `lang_path` works for your
paths.** The default assumes `/<locale>/...` structure.
## Configuration
```elixir
config :phoenix_page_meta,
# site-wide struct defaults (compile-time):
og_type: "website",
site_name: "MyApp",
twitter_site: "@myapp",
supported_locales: [:en, :es],
# extra struct fields + optional enforcement (compile-time):
extra_fields: [icon: nil, modal: false],
extra_enforce_keys: [],
# base URL resolution (runtime; auto-detected if omitted):
base_url: "https://example.com",
endpoint: MyAppWeb.Endpoint,
lang_path: &MyApp.I18n.lang_path/2
```