add dynamic routes plug and simplify router
Dynamic prefix rewriting for custom URL prefixes: - DynamicRoutes plug rewrites /p/123 to /products/123 - 301 redirects from canonical to custom prefix - Router simplified to 3 catch-all routes - LiveSandboxHook for test process ownership Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
101
lib/berrypod_web/plugs/dynamic_routes.ex
Normal file
101
lib/berrypod_web/plugs/dynamic_routes.ex
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
Reference in New Issue
Block a user