From e9649218fb81735391f75b53fd4f421aa9c484bd Mon Sep 17 00:00:00 2001 From: jamey Date: Wed, 1 Apr 2026 00:35:57 +0100 Subject: [PATCH] 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 --- lib/berrypod_web/plugs/redirects.ex | 60 +++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/berrypod_web/plugs/redirects.ex b/lib/berrypod_web/plugs/redirects.ex index e9f8875..5dfb3a5 100644 --- a/lib/berrypod_web/plugs/redirects.ex +++ b/lib/berrypod_web/plugs/redirects.ex @@ -8,14 +8,17 @@ defmodule BerrypodWeb.Plugs.Redirects do 3. Custom redirect lookup from the redirects table (ETS-cached) 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 alias Berrypod.Redirects + alias BerrypodWeb.R - # Only case-normalise paths under these prefixes (SEO-relevant shop routes). - # Paths with tokens, API keys, or other case-sensitive segments are excluded. - @lowercase_prefixes ~w(/products /collections /about /delivery /privacy /terms /search /cart /contact) + # Static prefixes that should always have the full path lowercased (page slugs) + @static_lowercase_prefixes ~w(/about /delivery /privacy /terms /search /cart /contact /checkout) def init(opts), do: opts @@ -28,9 +31,9 @@ defmodule BerrypodWeb.Plugs.Redirects do stripped != path -> redirect_to(conn, stripped, 301) - # Case mismatch on a shop path — redirect to lowercase - lowercase_path?(path) and String.downcase(path) != path -> - redirect_to(conn, String.downcase(path), 301) + # Case mismatch on a shop path — redirect to normalised form + (normalised = normalise_path(path)) != nil and normalised != path -> + redirect_to(conn, normalised, 301) # Check redirect table (ETS-cached) :else -> @@ -45,10 +48,51 @@ defmodule BerrypodWeb.Plugs.Redirects do end end - defp lowercase_path?(path) do - Enum.any?(@lowercase_prefixes, &String.starts_with?(String.downcase(path), &1)) + # Normalise path case. Returns the normalised path or nil if no normalisation needed. + # 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 + # 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 location = append_query(target, conn.query_string)