add url prefix settings with validation and redirects
Customisable route prefixes for products, collections, orders: - get_url_prefixes/0 and get_url_prefix/1 for retrieval - update_url_prefix/2 with format validation and conflict checks - Automatic redirects from old to new prefix URLs - list_product_slugs/0 and list_category_slugs/0 helpers - Redirects.clear_cache/0 for test isolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1004865013
commit
2fd7a0323e
@ -352,6 +352,29 @@ defmodule Berrypod.Products do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all product slugs.
|
||||
|
||||
Used for URL prefix redirects when admin changes the products prefix.
|
||||
"""
|
||||
def list_product_slugs do
|
||||
from(p in Product,
|
||||
where: p.visible == true and p.status == "active",
|
||||
select: p.slug
|
||||
)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all category slugs.
|
||||
|
||||
Used for URL prefix redirects when admin changes the collections prefix.
|
||||
"""
|
||||
def list_category_slugs do
|
||||
list_categories()
|
||||
|> Enum.map(& &1.slug)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recomputes denormalized fields from a product's variants.
|
||||
Called after variant sync to keep cached fields up to date.
|
||||
|
||||
@ -48,6 +48,15 @@ defmodule Berrypod.Redirects do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc "Clears the redirect cache without re-warming. Used in tests."
|
||||
def clear_cache do
|
||||
if :ets.whereis(@table) != :undefined do
|
||||
:ets.delete_all_objects(@table)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp invalidate_cache(from_path) do
|
||||
:ets.delete(@table, from_path)
|
||||
end
|
||||
@ -90,6 +99,10 @@ defmodule Berrypod.Redirects do
|
||||
attrs = Map.put(attrs, :to_path, to_path)
|
||||
from_path = attrs[:from_path] || attrs["from_path"]
|
||||
|
||||
# Don't create self-redirects
|
||||
if from_path == to_path do
|
||||
{:ok, :skipped}
|
||||
else
|
||||
# Flatten any existing redirects that point to our from_path
|
||||
flatten_incoming(from_path, to_path)
|
||||
|
||||
@ -105,6 +118,7 @@ defmodule Berrypod.Redirects do
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a manual redirect (from admin UI).
|
||||
|
||||
@ -307,6 +307,142 @@ defmodule Berrypod.Settings do
|
||||
end
|
||||
end
|
||||
|
||||
# ── URL prefixes ─────────────────────────────────────────────
|
||||
|
||||
@valid_prefix_types ~w(products collections orders)a
|
||||
@default_prefixes %{products: "products", collections: "collections", orders: "orders"}
|
||||
|
||||
@doc """
|
||||
Gets the current URL prefixes map.
|
||||
|
||||
Returns a map like `%{"products" => "p", "collections" => "c"}`.
|
||||
Missing keys use defaults.
|
||||
"""
|
||||
def get_url_prefixes do
|
||||
get_setting("url_prefixes") || %{}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the current prefix for a specific type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_url_prefix(:products)
|
||||
"p" # if customised, or "products" by default
|
||||
"""
|
||||
def get_url_prefix(type) when type in @valid_prefix_types do
|
||||
prefixes = get_url_prefixes()
|
||||
Map.get(prefixes, Atom.to_string(type)) || @default_prefixes[type]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a single URL prefix.
|
||||
|
||||
Creates redirects from old prefix URLs to new ones if products/collections exist.
|
||||
Invalidates the R module cache.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_url_prefix(:products, "p")
|
||||
{:ok, %{"products" => "p"}}
|
||||
"""
|
||||
def update_url_prefix(type, new_prefix)
|
||||
when type in @valid_prefix_types and is_binary(new_prefix) do
|
||||
new_prefix = String.downcase(String.trim(new_prefix))
|
||||
|
||||
# Validate prefix format
|
||||
cond do
|
||||
new_prefix == "" ->
|
||||
{:error, :empty_prefix}
|
||||
|
||||
not Regex.match?(~r/^[a-z0-9-]+$/, new_prefix) ->
|
||||
{:error, :invalid_format}
|
||||
|
||||
reserved_prefix?(new_prefix) ->
|
||||
{:error, :reserved_prefix}
|
||||
|
||||
true ->
|
||||
old_prefix = get_url_prefix(type)
|
||||
prefixes = get_url_prefixes()
|
||||
|
||||
# If same as current, no-op
|
||||
if old_prefix == new_prefix do
|
||||
{:ok, prefixes}
|
||||
else
|
||||
# Update the prefixes map
|
||||
# If new prefix matches default, remove the key (use default)
|
||||
new_prefixes =
|
||||
if new_prefix == @default_prefixes[type] do
|
||||
Map.delete(prefixes, Atom.to_string(type))
|
||||
else
|
||||
Map.put(prefixes, Atom.to_string(type), new_prefix)
|
||||
end
|
||||
|
||||
# Save to database
|
||||
{:ok, _} = put_setting("url_prefixes", new_prefixes, "json")
|
||||
|
||||
# Create redirects for existing items
|
||||
create_prefix_redirects(type, old_prefix, new_prefix)
|
||||
|
||||
# Invalidate R module cache
|
||||
BerrypodWeb.R.invalidate_all_sync()
|
||||
|
||||
{:ok, new_prefixes}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resets a URL prefix to its default value.
|
||||
"""
|
||||
def reset_url_prefix(type) when type in @valid_prefix_types do
|
||||
update_url_prefix(type, @default_prefixes[type])
|
||||
end
|
||||
|
||||
# Check if prefix conflicts with reserved routes
|
||||
defp reserved_prefix?(prefix) do
|
||||
reserved = ~w(admin api auth checkout coming-soon health live phoenix static _)
|
||||
prefix in reserved
|
||||
end
|
||||
|
||||
# Create redirects from old prefix to new prefix for existing items
|
||||
defp create_prefix_redirects(:products, old_prefix, new_prefix) do
|
||||
Berrypod.Products.list_product_slugs()
|
||||
|> Enum.each(fn slug ->
|
||||
Berrypod.Redirects.create_auto(%{
|
||||
from_path: "/#{old_prefix}/#{slug}",
|
||||
to_path: "/#{new_prefix}/#{slug}",
|
||||
source: "auto_prefix_change"
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
defp create_prefix_redirects(:collections, old_prefix, new_prefix) do
|
||||
Berrypod.Products.list_category_slugs()
|
||||
|> Enum.each(fn slug ->
|
||||
Berrypod.Redirects.create_auto(%{
|
||||
from_path: "/#{old_prefix}/#{slug}",
|
||||
to_path: "/#{new_prefix}/#{slug}",
|
||||
source: "auto_prefix_change"
|
||||
})
|
||||
end)
|
||||
|
||||
# Also redirect "all" and "sale" virtual collections
|
||||
for slug <- ["all", "sale"] do
|
||||
Berrypod.Redirects.create_auto(%{
|
||||
from_path: "/#{old_prefix}/#{slug}",
|
||||
to_path: "/#{new_prefix}/#{slug}",
|
||||
source: "auto_prefix_change"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp create_prefix_redirects(:orders, _old_prefix, _new_prefix) do
|
||||
# Orders use order numbers, which we can't enumerate easily
|
||||
# The DynamicRoutes plug handles the redirect at runtime
|
||||
:ok
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
||||
defp fetch_setting(key) do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user