# 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
```
