Public Routing
Off by default — most hosts already own their front-end and only want the editor. Flip on when you want FilamentCraft to serve published templates directly.

Enable the catch-all
// config/filamentcraft.php
'public_routes' => true,This registers a catch-all GET /{path?} that resolves the host (via Site::domain / subdomain) and renders the matching template's published revision. The homepage assignment in the editor topbar drives /. Slugs like /pricing resolve to the published template with that slug.
Tenant URL scheme
If you want a tenant URL scheme — /{tenant}/{slug} — keep public_routes => false and add one route:
use App\Models\Workspace;
use Illuminate\Support\Facades\Route;
Route::filamentCraftTenant(Workspace::class);That registers GET /{tenantSlug}/{path?}, resolves the Workspace by slug, binds the workspace's live FilamentCraft site, and renders the published page. The default route name is filamentcraft.tenant.public.
Use named arguments only when your app differs from the defaults:
Route::filamentCraftTenant(
ownerModel: Workspace::class,
tenantParameter: 'workspace',
tenantSlugColumn: 'domain_slug',
name: 'tenant.public',
);For completely custom URL schemes — /shop/{tenant}/{slug}, locale prefixes, channel paths, etc. — register your own route, bind the resolved Site into the container, then call [PublicSiteController::class, 'show'] with the remaining page path. For tenant-slug routes, prefer Route::filamentCraftTenant() unless your URL shape is truly custom.
Custom URL strategy — publicUrlsViaNamedRoute()
Hosts that own their routing (multi-tenant slugs, locale prefixes, channel paths) tell FilamentCraft how to build the public URL of a Template. This drives the editor topbar's "open live" pill, the URL bar, and the View live action on the Templates list — every place that asks "where does this page live?".
For the 90% case — a named Laravel route with a path parameter and an optional tenant parameter — use the declarative helper:
use FilamentCraft\FilamentCraftPlugin;
// Single-tenant: route('page.show', ['path' => $template->slug])
$plugin = FilamentCraftPlugin::make()
->publicUrlsViaNamedRoute('page.show');
// Multi-tenant with Route::filamentCraftTenant(...):
$plugin = FilamentCraftPlugin::make()
->publicUrlsViaNamedRoute(
'filamentcraft.tenant.public',
tenantParameter: 'tenantSlug',
);
// Locale-prefixed extras
$plugin = FilamentCraftPlugin::make()
->publicUrlsViaNamedRoute(
'tenant.public',
tenantParameter: 'tenantSlug',
extraParameters: [
'locale' => fn (\FilamentCraft\Models\Template $t) => $t->site?->locale,
],
);Pass the configured $plugin to $panel->plugin($plugin) in your panel provider; do not create a separate throwaway plugin instance.
Parameters: pathParameter (defaults to 'path'), tenantParameter (the route placeholder for the owner slug — null for single-tenant), tenantSlugColumn (the attribute on Site::owner holding the slug, defaults to 'slug'), homepagePath (the literal value for the homepage — '' by default; set to 'home' etc. if your route demands it), and extraParameters (scalar literals or closures resolved against the template at call time).
The result is memoised per-request on the Template instance, so the editor topbar, the templates list, the sitemap, and <og:url> tags all hit the resolver once — not once per call. The cache is cleared automatically on save() / refresh() so a status flip or slug rename invalidates it.
Or a raw closure — publicUrlUsing()
For exotic schemes (channel paths, custom domains per template, on-the-fly redirects), drop down to the closure form:
use FilamentCraft\Models\Template;
$plugin = FilamentCraftPlugin::make()
->publicUrlUsing(function (Template $template): ?string {
$owner = $template->site?->owner;
if (! $owner instanceof \App\Models\Team) {
return null; // fall through to default resolver
}
return route('tenant.public', [
'tenantSlug' => $owner->slug,
'path' => $template->isHomepage() ? '' : $template->slug,
]);
});Return null from either form to fall through to the default behaviour (catch-all route when public_routes is on, otherwise the auth'd preview URL). Partial overrides — "only when the site has an owner", "only when the locale is set" — degrade gracefully.
Linking to FilamentCraft pages — TemplateUrlPicker

TemplateUrlPicker — its dropdown lists the site's published templates to pick from.Need to let an admin pick a published page from a CTA button, a banner link, a footer menu item, or any other "open this page" field? Drop the picker into any Filament schema:
use FilamentCraft\Filament\Forms\Components\TemplateUrlPicker;
TemplateUrlPicker::make('cta_url')
->label('Destination page')
->required();Where do the options come from? The picker queries Template::query()->published() — every template whose status === Published and whose publicUrl() resolves to a non-empty string. The URL itself is whatever your publicUrlsViaNamedRoute() / publicUrlUsing() resolver returns (or the default catch-all / preview URL when no resolver is wired). Drafts are excluded — they have no public URL by design.
Defaults: options preload on open (no typing required), searchable filter by template name, scoped to the active Filament tenant, stores the resolved URL string so existing <a href> rendering keeps working without extra resolution code.
Configurable:
TemplateUrlPicker::make('cta_url')
->forSite($site->id) // pin to a specific site, ignoring the active tenant
->forAllSites() // list templates across every site (admin tools)
->searchLimit(20); // cap the dropdown size (defaults to 50)