PhoenixPageMeta
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
def deps do
[{:phoenix_page_meta, "~> 0.2"}]
end
With Igniter, the installer adds the config, the root.html.heex meta tags, and the LiveView wiring automatically:
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:
%PhoenixPageMeta.PageMeta{title: "Hello", path: "/hello"}
Configuration
All config is optional — add it to set site-wide defaults or extend the struct:
# 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_fieldsadds custom fields (bare atoms and{key, default}pairs may be mixed). Make some required withextra_enforce_keys: [...].base_urlis auto-detected from the single runningPhoenix.Endpoint. Setbase_url:(string,&Mod.url/0, or{m, f, a}) orendpoint: MyAppWeb.Endpointonly 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, runmix deps.compile phoenix_page_meta --force.
Wire LiveView
In MyAppWeb.live_view/0:
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:
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:
<head>
<PhoenixPageMeta.Components.MetaTags.default page_meta={@page_meta} />
<.live_title>{@page_title}</.live_title>
</head>
In nav components, use PageMeta.active?/2:
<.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):
<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:
- Move site-wide struct defaults into
config :phoenix_page_meta, and any custom fields intoextra_fields. - Replace
%MyAppWeb.PageMeta{}with%PhoenixPageMeta.PageMeta{}(or just re-point youralias). - Delete the
MyAppWeb.PageMetamodule.
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