PhoenixPageMeta

Hex.pmHexdocsCILicense

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:

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]

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:

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

FieldTypeNotes
:titleString.t()required
:pathString.t()required
:breadcrumb_titleString.t() | nilfalls back to :title in breadcrumbs
:parentt() | nilparent page; walked for breadcrumbs
:descriptionString.t() | nilmeta description, og:description, twitter:description
:og_imageString.t() | nilOG and Twitter image. Relative paths are auto-prefixed with base_url; absolute URLs (http:///https://) are rendered as-is
:og_image_altString.t() | nilalt text for og:image; falls back to :title
:og_typeString.t()default from config :phoenix_page_meta, :og_type (else "website")
:json_ldmap() | nilrendered as <script type="application/ld+json">
:canonical_pathString.t() | niloverrides :path for canonical URL
:noindexboolean()default false
:localeatom() | nilcurrent page's locale; renders og:locale
:supported_locales[atom()] | nilhreflang tags + og:locale:alternate; default from config
:site_nameString.t() | nilrenders og:site_name; default from config
:twitter_siteString.t() | nilrenders twitter:site (e.g. "@handle"); default from config
:skip_breadcrumbboolean() | nilwhen 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