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:
- First tries
translations->'language'->>'slug' = ? - 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
@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"
Checks if a category slug exists for a specific language.
Examples
iex> SlugResolver.category_slug_exists?("vases-planters", "en-US")
true
@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{}]
@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"}
@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 forlanguage- 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}
@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{}}
@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 foropts- 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}
@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 forlanguage- 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:
- Translated slug:
translations->'lang'->>'slug' - Canonical slug:
slugcolumn
This ensures backward compatibility with products that have no translations.
@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
@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{}]
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.
@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
Checks if a product slug exists for a specific language.
Useful for slug validation during product creation/editing.
Parameters
slug- The slug to checklanguage- Language codeexclude_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