update redirects plug for custom prefixes

Path normalisation respects custom URL prefixes:
- Orders only lowercase prefix, preserving case-sensitive order numbers
- Distinguishes static shop routes from dynamic prefixed routes
- Uses R module for prefix detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-04-01 00:35:57 +01:00
parent ecf84b81d1
commit e9649218fb

View File

@ -8,14 +8,17 @@ defmodule BerrypodWeb.Plugs.Redirects do
3. Custom redirect lookup from the redirects table (ETS-cached) 3. Custom redirect lookup from the redirects table (ETS-cached)
All redirects preserve query params. All redirects preserve query params.
Note: Order URLs only have their prefix lowercased, not the order number,
since order numbers contain uppercase hex characters (e.g. SS-260331-5D1A).
""" """
import Plug.Conn import Plug.Conn
alias Berrypod.Redirects alias Berrypod.Redirects
alias BerrypodWeb.R
# Only case-normalise paths under these prefixes (SEO-relevant shop routes). # Static prefixes that should always have the full path lowercased (page slugs)
# Paths with tokens, API keys, or other case-sensitive segments are excluded. @static_lowercase_prefixes ~w(/about /delivery /privacy /terms /search /cart /contact /checkout)
@lowercase_prefixes ~w(/products /collections /about /delivery /privacy /terms /search /cart /contact)
def init(opts), do: opts def init(opts), do: opts
@ -28,9 +31,9 @@ defmodule BerrypodWeb.Plugs.Redirects do
stripped != path -> stripped != path ->
redirect_to(conn, stripped, 301) redirect_to(conn, stripped, 301)
# Case mismatch on a shop path — redirect to lowercase # Case mismatch on a shop path — redirect to normalised form
lowercase_path?(path) and String.downcase(path) != path -> (normalised = normalise_path(path)) != nil and normalised != path ->
redirect_to(conn, String.downcase(path), 301) redirect_to(conn, normalised, 301)
# Check redirect table (ETS-cached) # Check redirect table (ETS-cached)
:else -> :else ->
@ -45,10 +48,51 @@ defmodule BerrypodWeb.Plugs.Redirects do
end end
end end
defp lowercase_path?(path) do # Normalise path case. Returns the normalised path or nil if no normalisation needed.
Enum.any?(@lowercase_prefixes, &String.starts_with?(String.downcase(path), &1)) # For order routes, only the prefix is lowercased (order numbers preserve case).
# For other routes, the entire path is lowercased.
defp normalise_path(path) do
lowered = String.downcase(path)
# Check static prefixes (full path lowercase)
if Enum.any?(@static_lowercase_prefixes, &String.starts_with?(lowered, &1)) do
lowered
else
# Check dynamic route prefixes
segments = path |> String.trim_leading("/") |> String.split("/")
first_segment = List.first(segments) || ""
prefix_type = R.prefix_type_from_segment(String.downcase(first_segment))
case prefix_type do
nil ->
# Not a dynamic route
nil
:orders ->
# Order routes: only lowercase the prefix, preserve order number case
normalise_order_path(segments)
_ ->
# Products/collections: lowercase entire path (slugs are always lowercase)
lowered
end
end
end end
# For order routes, only lowercase the first segment (the prefix)
defp normalise_order_path([prefix | rest]) do
lowered_prefix = String.downcase(prefix)
if lowered_prefix == prefix do
# Prefix already lowercase, no change needed
nil
else
"/" <> Enum.join([lowered_prefix | rest], "/")
end
end
defp normalise_order_path([]), do: nil
defp redirect_to(conn, target, status_code) do defp redirect_to(conn, target, status_code) do
location = append_query(target, conn.query_string) location = append_query(target, conn.query_string)