defmodule BerrypodWeb.Plugs.DynamicRoutes do @moduledoc """ Plug that enables dynamic route prefixes for products, collections, and orders. When a shop owner customises their URL prefixes (e.g., /products → /p), this plug: 1. **Rewrites incoming paths** - If the URL uses a custom prefix (/p/123), rewrites path_info to the canonical form (/products/123) so the router matches. 2. **Redirects old prefixes** - If the URL uses the old default prefix when a custom one is set (/products/123 when prefix is "p"), issues a 301 redirect to /p/123. This plug must run AFTER the Redirects plug (which handles trailing slashes and explicit redirects) but BEFORE the router. ## How it works The R module maintains an ETS cache of custom prefixes loaded from Settings. `R.prefix/1` returns the current prefix for a type, and `R.prefix_type_from_segment/1` performs reverse lookups. ## Example # Settings: url_prefixes = %{"products" => "p", "collections" => "c"} GET /p/123 → Rewrite to /products/123, router matches :product GET /products/123 → Redirect 301 to /p/123 GET /c/art → Rewrite to /collections/art, router matches :collection GET /collections/art → Redirect 301 to /c/art When no custom prefix is set, paths pass through unchanged. """ import Plug.Conn alias BerrypodWeb.R @default_prefixes %{ products: "products", collections: "collections", orders: "orders" } def init(opts), do: opts def call(conn, _opts) do case conn.path_info do [first_segment | rest] when rest != [] -> handle_prefixed_path(conn, first_segment, rest) _ -> conn end end defp handle_prefixed_path(conn, segment, rest) do prefix_type = R.prefix_type_from_segment(segment) current_prefix = prefix_type && R.prefix(prefix_type) default_prefix = prefix_type && @default_prefixes[prefix_type] cond do # No prefix type matched - not a dynamic route is_nil(prefix_type) -> conn # Using the current (possibly custom) prefix - rewrite to default for router segment == current_prefix and current_prefix != default_prefix -> rewrite_path(conn, default_prefix, rest, segment) # Using default prefix but custom is set - redirect to custom prefix segment == default_prefix and current_prefix != default_prefix -> redirect_to_custom(conn, current_prefix, rest) # Using the default prefix and no custom set, or already canonical true -> conn end end # Rewrite path_info so the router's static routes match # Store original path in conn.private for LiveView to use defp rewrite_path(conn, canonical_prefix, rest, original_prefix) do conn |> put_private(:original_prefix, original_prefix) |> put_private(:original_request_path, conn.request_path) |> Map.put(:path_info, [canonical_prefix | rest]) end # 301 redirect to the canonical custom prefix defp redirect_to_custom(conn, custom_prefix, rest) do path = "/" <> Enum.join([custom_prefix | rest], "/") location = append_query(path, conn.query_string) conn |> put_resp_header("location", location) |> send_resp(301, "") |> halt() end defp append_query(path, ""), do: path defp append_query(path, qs), do: "#{path}?#{qs}" end