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:
parent
6b90b394dd
commit
ecf84b81d1
@ -13,8 +13,8 @@ defmodule BerrypodWeb.Endpoint do
|
|||||||
]
|
]
|
||||||
|
|
||||||
socket "/live", Phoenix.LiveView.Socket,
|
socket "/live", Phoenix.LiveView.Socket,
|
||||||
websocket: [connect_info: [session: @session_options]],
|
websocket: [connect_info: [:user_agent, session: @session_options]],
|
||||||
longpoll: [connect_info: [session: @session_options]]
|
longpoll: [connect_info: [:user_agent, session: @session_options]]
|
||||||
|
|
||||||
# In prod, image variants and mockups live on the persistent volume
|
# In prod, image variants and mockups live on the persistent volume
|
||||||
# rather than inside the ephemeral release directory.
|
# rather than inside the ephemeral release directory.
|
||||||
@ -50,6 +50,11 @@ defmodule BerrypodWeb.Endpoint do
|
|||||||
plug Tidewave, allow_remote_access: true
|
plug Tidewave, allow_remote_access: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Enable sandbox for LiveView tests (allows LV processes to share test DB connection)
|
||||||
|
if Application.compile_env(:berrypod, :env) == :test do
|
||||||
|
plug Phoenix.Ecto.SQL.Sandbox
|
||||||
|
end
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
if code_reloading? do
|
if code_reloading? do
|
||||||
@ -76,5 +81,6 @@ defmodule BerrypodWeb.Endpoint do
|
|||||||
plug Plug.Head
|
plug Plug.Head
|
||||||
plug Plug.Session, @session_options
|
plug Plug.Session, @session_options
|
||||||
plug BerrypodWeb.Plugs.Redirects
|
plug BerrypodWeb.Plugs.Redirects
|
||||||
|
plug BerrypodWeb.Plugs.DynamicRoutes
|
||||||
plug BerrypodWeb.Plugs.BrokenUrlTracker, router: BerrypodWeb.Router
|
plug BerrypodWeb.Plugs.BrokenUrlTracker, router: BerrypodWeb.Router
|
||||||
end
|
end
|
||||||
|
|||||||
24
lib/berrypod_web/live_sandbox_hook.ex
Normal file
24
lib/berrypod_web/live_sandbox_hook.ex
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
defmodule BerrypodWeb.LiveSandboxHook do
|
||||||
|
@moduledoc """
|
||||||
|
On-mount hook that allows LiveView processes to use the Ecto SQL sandbox.
|
||||||
|
Only active in test environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Phoenix.LiveView
|
||||||
|
import Phoenix.Component
|
||||||
|
|
||||||
|
def on_mount(:default, _params, _session, socket) do
|
||||||
|
socket =
|
||||||
|
assign_new(socket, :phoenix_ecto_sandbox, fn ->
|
||||||
|
if connected?(socket), do: get_connect_info(socket, :user_agent)
|
||||||
|
end)
|
||||||
|
|
||||||
|
metadata = socket.assigns.phoenix_ecto_sandbox
|
||||||
|
|
||||||
|
if metadata do
|
||||||
|
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:cont, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
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
|
||||||
@ -275,21 +275,25 @@ defmodule BerrypodWeb.Router do
|
|||||||
{BerrypodWeb.PageEditorHook, :mount_page_editor},
|
{BerrypodWeb.PageEditorHook, :mount_page_editor},
|
||||||
{BerrypodWeb.NewsletterHook, :mount_newsletter}
|
{BerrypodWeb.NewsletterHook, :mount_newsletter}
|
||||||
] do
|
] do
|
||||||
live "/", Shop.Page, :home
|
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||||
live "/about", Shop.Page, :about
|
# │ Dynamic Shop Routes │
|
||||||
live "/delivery", Shop.Page, :delivery
|
# │ │
|
||||||
live "/privacy", Shop.Page, :privacy
|
# │ All shop pages are served by Shop.Page with runtime route resolution │
|
||||||
live "/terms", Shop.Page, :terms
|
# │ via BerrypodWeb.R. This enables shop owners to customise URLs. │
|
||||||
live "/contact", Shop.Page, :contact
|
# │ │
|
||||||
live "/collections/:slug", Shop.Page, :collection
|
# │ Single-segment pages (/:slug): │
|
||||||
live "/products/:id", Shop.Page, :product
|
# │ /cart, /about, /contact, /search, /delivery, /privacy, /terms, │
|
||||||
live "/cart", Shop.Page, :cart
|
# │ /orders, /checkout/success, plus custom CMS pages │
|
||||||
live "/search", Shop.Page, :search
|
# │ │
|
||||||
live "/checkout/success", Shop.Page, :checkout_success
|
# │ Two-segment pages (/:prefix/:id_or_slug): │
|
||||||
live "/orders", Shop.Page, :orders
|
# │ /products/:id, /collections/:slug, /orders/:number │
|
||||||
live "/orders/:order_number", Shop.Page, :order_detail
|
# │ (prefixes are customisable, e.g. /p/:id, /shop/:slug) │
|
||||||
|
# │ │
|
||||||
|
# │ See: R.default_page_type/1, R.default_prefix_type/1, @page_modules │
|
||||||
|
# └─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
# Catch-all for custom CMS pages — must be last
|
live "/", Shop.Page, :home
|
||||||
|
live "/:prefix/:id_or_slug", Shop.Page, :dynamic_prefix
|
||||||
live "/:slug", Shop.Page, :custom_page
|
live "/:slug", Shop.Page, :custom_page
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user