# `PhoenixKitEntities.UrlResolver`
[🔗](https://github.com/BeamLabEU/phoenix_kit_entities/blob/v0.4.6/lib/phoenix_kit_entities/url_resolver.ex#L1)

Logic for resolving entity and record URLs based on router introspection,
entity settings, and global configuration.

Extracted from SitemapSource to provide a shared API for public URL generation.

# `routes_cache`

```elixir
@type routes_cache() :: %{
  optional(:all_routes) =&gt; list(),
  optional(:content_routes) =&gt; list(),
  optional(:index_routes) =&gt; list(),
  entity_patterns: map(),
  entity_index_paths: map()
}
```

# `add_public_locale_prefix`

```elixir
@spec add_public_locale_prefix(String.t(), String.t() | nil) :: String.t()
```

Adds a language prefix for public front-end URLs.

Policy:
- Single-language mode → no prefix
- Locale is `nil`, empty, or malformed → no prefix
- Locale matches the primary language → follows the site-wide
  `Languages.default_language_no_prefix?/0` setting (when ON, skip
  the prefix; when OFF, include it — `/<base>`)
- Otherwise → `/<base>` prefix (where `<base>` is a validated base code)

This matches the convention used by `PhoenixKit.Utils.Routes.path/2`
and `PhoenixKit.Utils.Routes.admin_path/2`, which both honor the same
setting.

The base code is validated against `^[a-z]{2,3}$` before interpolation, so
caller-supplied locales (for example from request params) cannot inject
arbitrary path segments.

# `build_path`

```elixir
@spec build_path(String.t(), map()) :: String.t()
```

Substitutes `:slug` and `:id` placeholders in a URL pattern with record data.

`:slug` falls back to the record UUID when the slug is nil. Patterns without
placeholders are returned unchanged.

# `build_path_with_language`

```elixir
@spec build_path_with_language(String.t(), String.t() | nil, boolean()) :: String.t()
```

Adds a language prefix to a path for the sitemap.

Used by `PhoenixKitEntities.SitemapSource` for hreflang-aware sitemap
entries. The prefix decision is delegated to the framework-shared
`PhoenixKit.Modules.Sitemap.LocalePath.emit_prefix?/2` (the same policy
every core sitemap source uses), so entities' sitemap stays consistent
with publishing/posts/static. This helper owns only the formatting.

Pass `is_default: true` only when `language` is the site's primary —
`emit_prefix?/2` trusts that flag. Consumers building public links
should prefer `add_public_locale_prefix/2`, which derives primary-ness
from the locale itself.

Resolves the site-wide locale settings on every call. When building
many URLs for the same `(language, is_default)` pair — e.g. a sitemap
run — resolve `locale_prefix/2` once and prepend it instead.

# `build_routes_cache`

```elixir
@spec build_routes_cache() :: routes_cache()
```

Builds a cache of all routes for efficient lookups.

For hot loops (e.g. rendering a listing of records), build the cache once
via this function and pass it as `:routes_cache` to `EntityData.public_path/3`.

# `build_url`

```elixir
@spec build_url(String.t(), String.t() | nil) :: String.t()
```

Builds a full URL by prepending a base URL.

If `base_url` is nil, falls back to the `site_url` setting (or empty string).

# `get_index_path_cached`

```elixir
@spec get_index_path_cached(map(), routes_cache()) :: String.t() | nil
```

Resolves the index-page path for an entity using a pre-built routes cache.

Used by the sitemap source to emit index entries (e.g. `/products` alongside
`/products/:slug`). Returns `nil` if no index path can be resolved.

# `get_url_pattern_cached`

```elixir
@spec get_url_pattern_cached(map(), routes_cache()) :: String.t() | nil
```

Resolves the URL pattern for an entity using a pre-built routes cache.

Resolution chain: `entity.settings["sitemap_url_pattern"]` → router introspection
(explicit or catchall) → `sitemap_entity_<name>_pattern` setting → global
`sitemap_entities_pattern`. Returns `nil` if none match.

# `locale_prefix`

```elixir
@spec locale_prefix(String.t() | nil, boolean()) :: String.t()
```

Resolves the constant locale path-prefix for a `(language, is_default)`
pair — `"/en"`, `""`, etc.

The decision (via `LocalePath.emit_prefix?/2`) and base-code extraction
read site-wide language settings, which are invariant across a single
sitemap generation. Resolve this once and prepend the result to each
record's path rather than calling `build_path_with_language/3` per URL,
which re-reads those settings every time.

# `single_language_mode?`

```elixir
@spec single_language_mode?() :: boolean()
```

Returns `true` when the site is effectively single-language.

True when the Languages module is disabled or only one language is enabled;
also true if the lookup fails (defensive fallback).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
