PhoenixKit.Modules.Shop.SlugResolver (phoenix_kit v1.7.71)

Copy Markdown View Source

Resolves URL slugs to Products and Categories with language awareness.

This module provides language-aware slug resolution for the Shop module, supporting per-language URL slugs for SEO optimization.

Features

  • Per-language SEO-friendly URL slugs
  • Fallback to canonical slug when translation not found
  • Base code matching (e.g., "en" matches "en-US")
  • Efficient queries using JSONB operators

URL Architecture

/shop/products/geometric-planter           # Default language
/es/shop/products/maceta-geometrica        # Spanish (SEO slug)
/ru/shop/products/geometricheskoe-kashpo   # Russian (SEO slug)

Usage Examples

# Find product by slug in specific language
SlugResolver.find_product_by_slug("maceta-geometrica", "es-ES")
# => {:ok, %Product{}}

# Find product with base code (resolves to full dialect)
SlugResolver.find_product_by_slug("geometric-planter", "en")
# => {:ok, %Product{}} (matches en-US via base code)

# Find category by slug
SlugResolver.find_category_by_slug("jarrones-macetas", "es-ES")
# => {:ok, %Category{}}

Query Behavior

The resolver checks both translated slugs and canonical slugs:

  1. First tries translations->'language'->>'slug' = ?
  2. Falls back to canonical slug = ?

This ensures URLs work even for products without translations.

Summary

Functions

Gets the best slug for a category in a specific language.

Checks if a category slug exists for a specific language.

Finds multiple categories by their slugs for a specific language.

Finds a category by slug in any language.

Finds a category by URL slug for a specific language.

Finds a category by slug, requiring exact language match.

Finds a product by slug in any language.

Finds a product by URL slug for a specific language.

Finds a product by slug, requiring exact language match.

Finds multiple products by their slugs for a specific language.

Normalizes a language code to dialect format.

Gets the best slug for a product in a specific language.

Checks if a product slug exists for a specific language.

Functions

category_slug(category, language)

@spec category_slug(PhoenixKit.Modules.Shop.Category.t(), String.t()) ::
  String.t() | nil

Gets the best slug for a category in a specific language.

Returns translated slug if available, otherwise canonical slug.

Examples

iex> SlugResolver.category_slug(category, "es-ES")
"jarrones-macetas"

category_slug_exists?(slug, language, opts \\ [])

@spec category_slug_exists?(String.t(), String.t(), keyword()) :: boolean()

Checks if a category slug exists for a specific language.

Examples

iex> SlugResolver.category_slug_exists?("vases-planters", "en-US")
true

find_categories_by_slugs(url_slugs, language, opts \\ [])

@spec find_categories_by_slugs([String.t()], String.t(), keyword()) :: [
  PhoenixKit.Modules.Shop.Category.t()
]

Finds multiple categories by their slugs for a specific language.

Examples

iex> SlugResolver.find_categories_by_slugs(["cat-1", "cat-2"], "en-US")
[%Category{}, %Category{}]

find_category_by_any_slug(url_slug, opts \\ [])

@spec find_category_by_any_slug(
  String.t(),
  keyword()
) ::
  {:ok, PhoenixKit.Modules.Shop.Category.t(), String.t()} | {:error, :not_found}

Finds a category by slug in any language.

Examples

iex> SlugResolver.find_category_by_any_slug("jarrones-macetas")
{:ok, %Category{}, "es"}

find_category_by_slug(url_slug, language, opts \\ [])

@spec find_category_by_slug(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKit.Modules.Shop.Category.t()} | {:error, :not_found}

Finds a category by URL slug for a specific language.

Parameters

  • url_slug - The URL slug to search for
  • language - Language code (supports both full and base codes)
  • opts - Optional keyword list:
    • :preload - Associations to preload (default: [])
    • :status - Filter by status (e.g., "active")

Examples

iex> SlugResolver.find_category_by_slug("jarrones-macetas", "es-ES")
{:ok, %Category{name: "Jarrones y Macetas", ...}}

iex> SlugResolver.find_category_by_slug("vases-planters", "en")
{:ok, %Category{}}

iex> SlugResolver.find_category_by_slug("nonexistent", "en-US")
{:error, :not_found}

find_category_by_translated_slug(url_slug, language, opts \\ [])

@spec find_category_by_translated_slug(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKit.Modules.Shop.Category.t()} | {:error, :not_found}

Finds a category by slug, requiring exact language match.

Does not fall back to canonical slug.

Examples

iex> SlugResolver.find_category_by_translated_slug("jarrones-macetas", "es-ES")
{:ok, %Category{}}

find_product_by_any_slug(url_slug, opts \\ [])

@spec find_product_by_any_slug(
  String.t(),
  keyword()
) ::
  {:ok, PhoenixKit.Modules.Shop.Product.t(), String.t()} | {:error, :not_found}

Finds a product by slug in any language.

Searches across all translated slugs to find the product. Useful for cross-language redirect when user visits with a slug from a different language.

Parameters

  • url_slug - The URL slug to search for
  • opts - Optional keyword list:
    • :preload - Associations to preload (default: [])
    • :status - Filter by status (e.g., "active")

Examples

iex> SlugResolver.find_product_by_any_slug("maceta-geometrica")
{:ok, %Product{}, "es"}  # Returns product with language that matched

iex> SlugResolver.find_product_by_any_slug("geometric-planter")
{:ok, %Product{}, "en"}

iex> SlugResolver.find_product_by_any_slug("nonexistent")
{:error, :not_found}

find_product_by_slug(url_slug, language, opts \\ [])

@spec find_product_by_slug(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKit.Modules.Shop.Product.t()} | {:error, :not_found}

Finds a product by URL slug for a specific language.

Parameters

  • url_slug - The URL slug to search for
  • language - Language code (supports both "es-ES" and base codes like "en")
  • opts - Optional keyword list:
    • :preload - Associations to preload (default: [])
    • :status - Filter by status (e.g., "active")

Examples

iex> SlugResolver.find_product_by_slug("maceta-geometrica", "es-ES")
{:ok, %Product{title: "Maceta Geométrica", ...}}

iex> SlugResolver.find_product_by_slug("geometric-planter", "en")
{:ok, %Product{}}  # Matches en-US via base code resolution

iex> SlugResolver.find_product_by_slug("nonexistent", "en-US")
{:error, :not_found}

Query Details

The query checks both:

  1. Translated slug: translations->'lang'->>'slug'
  2. Canonical slug: slug column

This ensures backward compatibility with products that have no translations.

find_product_by_translated_slug(url_slug, language, opts \\ [])

@spec find_product_by_translated_slug(String.t(), String.t(), keyword()) ::
  {:ok, PhoenixKit.Modules.Shop.Product.t()} | {:error, :not_found}

Finds a product by slug, requiring exact language match.

Unlike find_product_by_slug/3, this does not fall back to canonical slug. Useful when you need to ensure the translation exists.

Examples

iex> SlugResolver.find_product_by_translated_slug("maceta-geometrica", "es-ES")
{:ok, %Product{}}

iex> SlugResolver.find_product_by_translated_slug("maceta-geometrica", "en-US")
{:error, :not_found}  # No fallback to canonical

find_products_by_slugs(url_slugs, language, opts \\ [])

@spec find_products_by_slugs([String.t()], String.t(), keyword()) :: [
  PhoenixKit.Modules.Shop.Product.t()
]

Finds multiple products by their slugs for a specific language.

Useful for preloading products in listing pages.

Examples

iex> SlugResolver.find_products_by_slugs(["planter-1", "planter-2"], "en-US")
[%Product{}, %Product{}]

normalize_language_public(lang)

Normalizes a language code to dialect format.

Converts base codes to full dialect (e.g., "en" -> "en-US"). Used by import system to ensure consistent language keys in JSONB fields.

product_slug(product, language)

@spec product_slug(PhoenixKit.Modules.Shop.Product.t(), String.t()) ::
  String.t() | nil

Gets the best slug for a product in a specific language.

Returns translated slug if available, otherwise canonical slug.

Examples

iex> SlugResolver.product_slug(product, "es-ES")
"maceta-geometrica"

iex> SlugResolver.product_slug(product, "fr-FR")
"geometric-planter"  # Falls back to canonical

product_slug_exists?(slug, language, opts \\ [])

@spec product_slug_exists?(String.t(), String.t(), keyword()) :: boolean()

Checks if a product slug exists for a specific language.

Useful for slug validation during product creation/editing.

Parameters

  • slug - The slug to check
  • language - Language code
  • exclude_uuid - Product UUID to exclude from check (for edits)

Examples

iex> SlugResolver.product_slug_exists?("geometric-planter", "en-US")
true

iex> SlugResolver.product_slug_exists?("geometric-planter", "en-US", exclude_uuid: "some-uuid")
false  # Excludes product with given UUID from check