Skip to main content

Documentation

No results found.
Features

File-Based Routing

WebProCMS treats every public page as a real Laravel route. When you create a page in the dashboard editor, the editor writes a literal Route::livewire(...) line into routes/web.php — no parallel "pages" database table, no catch-a...

WebProCMS treats every public page as a real Laravel route. When you create a page in the dashboard editor, the editor writes a literal Route::livewire(...) line into routes/web.php — no parallel "pages" database table, no catch-all controller, no slug-to-component lookup at request time. The route file is your sitemap, and it lives in version control alongside everything else.


The problem

Most CMSes — WordPress, October, traditional Craft, classic Drupal — route every URL through a single catch-all handler. When a request comes in, the framework hits the database first to ask "does a page exist at this slug?" then renders dynamically. That design has three costs that compound:

  1. Every page hit pays a database round-trip just to find itself. Before the page can even start rendering, the CMS has to look up a row by URL. On a cold cache that's the slowest part of the request. WordPress sites add aggressive page-caching plugins to mask this — the bottleneck doesn't go away, it just gets a workaround.
  2. Standard framework tooling stops working. php artisan route:list shows no pages, because pages aren't routes. route('home') doesn't resolve — you have to write Page::where('slug', 'home')->first()->url. Auth middleware, response cache middleware, role checks — none of them attach to "a page" naturally; each one needs a CMS-side shim.
  3. The site map lives in the database. There's no git log for URL changes, no diffing between staging and production, no code review of "we added a /pricing page" — those are database operations that happen at runtime, invisible to version control. When something goes wrong, the truth is in a row somewhere, not in a file.

The fix

Pages are first-class Laravel routes. When you create /about through the page editor:

  1. The editor writes a new line to routes/web.php via VoltFileService:
    Route::livewire('about', 'pages::about')->name('about');
    
  2. The editor writes the matching Volt component to resources/views/pages/⚡about.blade.php.
  3. That's it. Laravel discovers the route the next time it boots; route('about') resolves; navigation between pages works.

No database lookup happens to find the page at request time. The route is registered at boot like every other route in your application, and matched at request time by Laravel's normal route dispatcher. The CMS doesn't pay any "is this a CMS page?" tax on top of standard Laravel routing.

Side benefits

php artisan route:list is your sitemap

Every page shows up alongside framework routes — with its URL, name, middleware stack, and the Volt component that renders it. New employees onboarding to a site can run php artisan route:list --except-vendor and see the entire URL structure in one place. No "where do I look up the pages?" question.

route('home') works everywhere

Page URLs resolve through Laravel's standard URL generator. From PHP, Blade, JavaScript-via-Ziggy, or anywhere else — route('locations.show', $location) is a real thing the framework can produce, not a hand-rolled lookup. Page links survive renames because they're keyed by route name, not by slug string.

Middleware composes naturally

Need a members-only page? Wrap the route in auth + role:member middleware — Laravel's normal pattern, no CMS plugin:

Route::middleware(['auth', 'role:member'])->group(function () {
    Route::livewire('premium', 'pages::premium')->name('premium');
});

Need response caching? Attach Spatie\ResponseCache\Middlewares\CacheResponse to the route group, same way you'd cache any other Laravel route. That's how the stubbed routes/web.php already wraps the cached-page routes.

Page changes are in git

Every URL change is a routes/web.php diff that lands as a real commit. You can git log routes/web.php and see when /contact was added; you can git blame to find which deploy introduced a redirect; you can git diff main staging to see what URLs differ between environments. Compare to DB-backed CMSes where URL changes happen invisibly in production and there's no equivalent audit trail.

The "out of sync" failure mode doesn't exist

In DB-backed CMSes, a common bug is "the page exists in the DB but the slug doesn't match the actual URL the user typed" or "the page was deleted but a redirect still points at it." With file-based routing there's no second source of truth to drift out of sync — either the line is in routes/web.php and the page exists, or it isn't and the page doesn't.

Trade-offs (named honestly)

A mutable file in version control is a sharp tool, and it cuts in two directions.

The cost: routes/web.php has to be writable at runtime by the web server, and the file is gitignored so each install can mutate it independently without conflicting with git pull during CMS updates. That means a fresh git clone doesn't ship with any page routes — the file has to be materialised from a stub on first install. If the stub or the materialisation step breaks, fresh installs end up with a website that 404s.

The mitigation: The seeder (ClientPageSeeder) delegates to ThemeInstaller, which copies the active theme's routes.php (e.g. resources/themes/default/routes.php) into place as routes/web.php on fresh install. That file is tracked in git, ships with every clone, and contains the full set of default routes (search, sitemap, robots, llms.txt, plus the theme's default pages). After the first install, the editor mutates the live file via surgical preg_replace against named insertion markers — never destructive, never overwrites unrelated lines.

The "tweak the defaults" workflow: Edit your local pages in the dashboard editor the same way you'd edit any client install. When the defaults look the way you want, run php artisan themes:capture {slug} — it snapshots the live pages, routes/web.php, navigation, and any referenced shared rows back into resources/themes/{slug}/, ready to commit. Next fresh install picks up the new defaults; existing installs are untouched (ThemeInstaller never overwrites existing pages).

Single-site forks: track everything in git

The default .gitignore treats routes/web.php, resources/views/pages/⚡*.blade.php, resources/views/shared-rows/*.blade.php, config/navigation.php, and resources/views/layouts/partials/*.blade.php as per-install runtime state — because the canonical WebProCMS repo is meant to be cloned by many different installs, each with their own edits, and upstream doesn't want client A's pages leaking into the codebase that ships to client B.

If you fork WebProCMS to run one specific site, that calculus flips. Those files aren't "runtime state to be hidden from the repo" anymore — they're your site's actual content, and tracking them in git is strictly better than not. The fork can drop those .gitignore entries:

- /resources/views/pages/⚡*.blade.php
+ !/resources/views/pages/⚡*.blade.php
- /resources/views/pages/*/⚡*.blade.php
- /resources/views/shared-rows/*.blade.php
- /routes/web.php
- /config/navigation.php
- /resources/views/layouts/partials/*.blade.php

Then git add -A, commit, and now the entire site — pages, routes, layouts, shared rows, navigation config — lives in version control. What that unlocks:

  • Real diffs on page edits. "Updated home hero copy" becomes a reviewable commit on the same timeline as code changes.
  • Multi-server deploys are trivial. git push to the server, git pull on the receivers. No rsync between editor host and read replicas, no shared filesystem mount — Git is the sync mechanism.
  • Rollback works on content too. A bad page edit is a git revert away. Compare to "I edited something in production three days ago and now I can't remember which page or how to put it back."
  • Staging mirrors prod by definition. git checkout a SHA on staging and you have an exact replica of prod at that point in time — including every page's exact content.
  • Page edits land alongside code changes. If you ship a feature that requires a new page, both move through the same PR and deploy together.

The trade-off: the fork loses easy upstream git pull from canonical WebProCMS, because the un-ignored files now overlap with paths upstream still considers runtime-only. The fork would typically pin to a WebProCMS version, update deliberately on a schedule, and resolve any conflicts when they appear. For a single-site operator, that's a small price for the version-control benefits.

This pattern is the recommended approach for anyone forking WebProCMS to power one specific site rather than to operate a multi-tenant CMS host.

Multi-server installs

Because routes/web.php is per-install and per-server filesystem, multi-server deployments need a single source of truth. Two patterns work:

  1. Shared filesystem. Mount routes/web.php from a shared volume that all web servers read. Editor writes once, every server sees the update on the next request.
  2. Single editor server + rsync. Designate one server as the editor host; other servers rsync routes/web.php, resources/views/pages/⚡*.blade.php, and resources/views/shared-rows/ from it on a schedule (or in response to a webhook fired by the editor).

See docs/multi-server.md for the full multi-server runbook.

Where this lives in the code

  • bootstrap/app.php — points Laravel at routes/web.php for public routes, routes/cms.php for dashboard/admin routes (which is not runtime-mutated).
  • resources/themes/default/routes.php — the default theme's baseline routes file, copied into place on fresh install. Contains the default page routes plus utility endpoints (search, sitemap, robots, llms).
  • app/Support/VoltFileService.php — the surgery that adds/removes/renames route lines when the dashboard editor creates/deletes/clones a page. Uses named comment markers (// new cached pages are inserted here) as insertion anchors.
  • database/seeders/ClientPageSeeder.php — delegates to ThemeInstaller, which copies the active theme's routes.php into place on first install.
  • app/Console/Commands/ThemesCaptureCommand.php — the maintainer-side themes:capture command that snapshots current pages + routes back into resources/themes/{slug}/ (the successor to the retired stubs:capture-pages).

The CMS dashboard, settings pages, and design editor all live in routes/cms.php — that file is tracked in git, never modified by the editor, and is the same on every install. Only the public-page routes are per-install. The split keeps the CMS core stable while the client-editable zone stays version-controlled.