Chapter 15 — v0.31 — tenant admin no longer catches every URL
The big architectural fix this cycle. Through v0.30 the tenancy
server::Builder
attached the tenant admin as Router::fallback_service(...) on the
merged user router. Axum semantics: that overrides any
.fallback() set inside the user's API router — so a
CMS-style public site at / was impossible. Every unmatched URL
got the admin's /{table} catch-all and returned
{"error":"table not found"} instead of running the user's
resolver.
What changed
The framework now mounts the admin via explicit routes (see
build_admin_routes).
The fallback_service is gone. Routes claimed by the admin:
routes.admin_url+routes.admin_url/+routes.admin_url/{*rest}— admin properroutes.login_url,routes.logout_url,routes.change_password_url,routes.impersonation_handoff_urlroutes.static_url/{*rest},routes.brand_url/{*rest}/__end-impersonation(hardcoded fallback insidehandle_request)- Legacy
/__admin*mounts for back-compat withRouteConfig::legacy()apps
Everything else falls through to the user's .fallback() (or 404
if no fallback is set).
What this enables
The headline use case is a CMS-style public site on the same
tenant subdomain as the admin. The companion rustango-cms 0.1
crate ships a working setup:
let mut tera = Tera::new(&templates_glob)?;
rustango_cms::admin::register_templates(&mut tera)?;
let tera = std::sync::Arc::new(tera);
// CMS admin at /cms-admin/...; public pages at the site root.
let api = rustango_cms::admin::router(tera.clone())
.merge(rustango_cms::router(tera));
rustango::manage::Cli::new()
.tenancy()
.api(api)
.seed(|registry| async move {
rustango_cms::ensure_seeded(®istry).await?;
Ok(())
})
.run()
.await
After this:
/→ CMS root page/<slug>→ CMS resolver looks up the page/admin/...→ tenant admin/cms-admin/pages→ CMS-aware admin (path/depth/sort_order computed correctly, type whitelists enforced)/random-thing→ CMS resolver returnsPage not found: …(404, not the admin's{"error":"table not found"})
Migration
| App shape | Behavior change |
|---|---|
Custom routes + .fallback() (CMS-style) | Fallback now runs for unmatched URLs. If you worked around the bug with explicit /{*path} wildcards, you can simplify. |
| Just rustango admin, no custom routes | /random-url now returns 404 instead of admin's {"error":"table not found"} JSON. |
Custom routes, no .fallback() | Same as above — 404 for unclaimed URLs. |
Hardcoded /admin/* or /__admin/* links | Unchanged. |
| Apps that intentionally relied on the admin catching random URLs | Will break — set a custom .fallback() on your API router to keep the old behavior. |
Companion fixes shipped in rustango-cms 0.1
The rustango-cms admin was unusable against the v0.30
serialization shape; v0.31's matching rustango-cms release
fixes the template / handler bugs that surfaced building the
end-to-end demo:
- Template
.Setreferences —Auto<T>now serializes as the bare value (e.g.1), not enum-tagged{"Set": 1}. The R-CMS admin templates were stuck on the old shape and 500'd withVariable t.id.Set not found in context. Replaced with{{ x.id }}everywhere. - Edit-form action URL — the form POSTed to
/cms-admin/pages/{id}but the actual route is/cms-admin/pages/{id}/edit. Saving a page worked because the redirect-chain mostly worked out; the underlying mismatch was real. slugfieldrequiredattribute — root pages need an empty slug (the resolver matchesWHERE slug = '') but the form blocked empty submit. Therequiredis now conditional onparentso root creation works.AdminError::IntoResponsewalksError::source()so Tera errors surface the actual cause line instead of the generic "Failed to render 'template.html'".render(t, tera, page, url_prefix)— newurl_prefixparameter, injected as{{ url_prefix }}into the Tera context so user templates can build breadcrumb / sibling links without hardcoding the host's URL layout.router_at(prefix, tera)— kept alongsiderouter(tera)for projects that want their CMS at a non-root prefix (e.g./blog/alongside other site content). Includes a permanent 308 redirect for{prefix}/→{prefix}to handle axum's strict trailing-slash matching.- "View live ↗" button on every published row of the CMS
admin's page list, and on the edit form header. URLs are
pre-computed server-side via a single-pass
build_live_url_mapwalk in tree order.