Routing strategy that lets publishing's catch-all coexist with host
routes shaped /:locale/something/... declared after phoenix_kit_routes().
Why this exists
Publishing's public URLs are dynamic — /:language/:group/*path where
:group is any DB row. Phoenix.Router has no per-segment regex
constraint, so the catch-all matches every two-or-more-segment URL.
Phoenix matches in declaration order with no fall-through, which means
any host route declared after phoenix_kit_routes() and shaped
/:locale/<literal>/... is silently shadowed: publishing's controller
matches first, doesn't find the group, returns 404.
How this works
Instead of registering the catch-all directly under /, publishing
registers it under an internal prefix (/__phoenix_kit_publishing_dispatch).
The host router's call/2 is overridden (via Phoenix.Router's documented
defoverridable init: 1, call: 2) to:
- Check if the request's first non-locale path segment is a known
publishing group via
maybe_rewrite/1. - If yes — rewrite
conn.path_infoandconn.request_pathto prepend the internal prefix;super(conn, opts)then matches the internal-prefix route and runs the full Phoenix pipeline normally. - If no — pass through unchanged; Phoenix matches host routes.
After the route matches, restore_path/2 (a pipeline plug) un-mutates
request_path and path_info so controllers reading conn.request_path
for canonical URL generation see the URL the client sent, not the
internal prefix.
Phoenix's pipelines run via the internal-prefix scope's pipe_through,
so :browser (sessions, CSRF, layout), :phoenix_kit_* (auth, locale),
and any host-defined plugs are applied via the standard mechanism — no
manual replication, no telemetry gap.
Trade-offs
mix phx.routesshows the routes under the internal prefix. Devs debugging "where does/blog/postgo?" need to know to look for__phoenix_kit_publishing_dispatch. Documented in core AGENTS.md.- Per-request cost: one DB lookup on each request. ETS/
:persistent_termcaching is a future optimization (the DB read is small + indexed).
Summary
Functions
Internal prefix segment used in path rewriting.
Discriminator segment for URLs that have a leading locale (i.e. the
group slug is at path_info[1]). Phoenix routes inside this sub-scope
bind :language + :group per publishing's localized form.
Decide whether conn is bound for publishing.
Pipeline plug — restores conn.request_path and conn.path_info to
the un-rewritten form once Phoenix has extracted route bindings into
conn.params.
Discriminator segment for URLs that have NO leading locale (i.e. the
group slug is at path_info[0]). Phoenix routes inside this sub-scope
bind :group only per publishing's non-localized form.
Functions
@spec internal_prefix() :: String.t()
Internal prefix segment used in path rewriting.
@spec localized_segment() :: String.t()
Discriminator segment for URLs that have a leading locale (i.e. the
group slug is at path_info[1]). Phoenix routes inside this sub-scope
bind :language + :group per publishing's localized form.
@spec maybe_rewrite(Plug.Conn.t()) :: {:rewrite, Plug.Conn.t()} | :pass
Decide whether conn is bound for publishing.
Returns {:rewrite, conn} with conn.path_info and conn.request_path
prepended with the internal prefix + a discriminator segment that
picks publishing's localized vs non-localized route shape:
- If
path_info[1]is a known group → rewrite under__phoenix_kit_publishing_dispatch/localized/...so Phoenix matches/:language/:group(/*path)(the URL has a leading locale). - Else if
path_info[0]is a known group → rewrite under__phoenix_kit_publishing_dispatch/root/...so Phoenix matches/:group(/*path)(no leading locale).
Returns :pass if neither resolves to a known group. The dual
discriminator is load-bearing — without it, both /:language/:group
and /:group/*path would match a 2-segment internal path, and
Phoenix's first-match-wins picks the localized form even when the
URL had no locale prefix. That sends the request to the controller
with language=<group-slug>, group=<post-slug> and the lookup fails.
The DB lookup is small and indexed; a future optimization would
cache the slug set in :persistent_term.
@spec restore_path( Plug.Conn.t(), keyword() ) :: Plug.Conn.t()
Pipeline plug — restores conn.request_path and conn.path_info to
the un-rewritten form once Phoenix has extracted route bindings into
conn.params.
Without this, controllers that compute canonical URLs from
conn.request_path (publishing's default_language_no_prefix redirect
is one) include the internal prefix in their Location header,
causing a redirect loop on the next request.
@spec root_segment() :: String.t()
Discriminator segment for URLs that have NO leading locale (i.e. the
group slug is at path_info[0]). Phoenix routes inside this sub-scope
bind :group only per publishing's non-localized form.