🧭 Routing in PageBlocks
Routes define which code should execute when a specific URL is accessed on your site.
Example: if a user visits /about, you can set up a route to display the "About Us" page.
🔰 Basic Example
PageBlocks uses the Route facade to define routes:
use Boshnik\PageBlocks\Facades\Route;
Route::get('/about', function () {
return 'This is the About page';
});📥 When someone opens https://example.com/about, this function runs, and the user sees its output.
You can return the following from a route:
| Type | Result |
|---|---|
string | Plain text |
array | JSON |
object | JSON or HTML (depending on content and headers) |
Response | Explicit response object using the Response class |
Everything returned from a route automatically passes through
Response::dispatchResponse, which determines how to properly send the result to the client.
📂 Where Are Routes Located?
All routes are stored in the core/App/routes folder of your project:
core/
└── App/
└── routes/
├── web.php
├── api.php
└── ...🔄 All .php files in this folder are automatically loaded, and their routes are registered when the application starts.
📝 You can split routes into multiple files:
web.php— Main website routesapi.php— API routesadmin.php,auth.php,blog.php— Any additional route groups
The filename doesn’t matter as long as it’s in
core/App/routesand has a.phpextension. One exception: a file whose name starts with_is skipped, which is a handy way to park a set of routes without deleting it.
⚙️ Who handles the front end — pageblocks_routing
The system setting decides how much of the site the component's router owns. It ships as full:
| Value | In the combo | What happens |
|---|---|---|
full | Full API | The router sees every front-end request. The shipped core/App/routes/web.php ends with Route::fallback('ResourceController@index'), so a URI that matches no route of yours is still rendered by PageBlocks. Remove that fallback and an unmatched URI becomes a 404. |
routes | Route Only | The router sees the request, but a URI that matches nothing (and has no fallback) is handed back to MODX, which resolves it as an ordinary resource. |
off | Disabled | The front end is left to MODX entirely. |
off does not switch the component off — only its front end
Requests under mgr/ keep going through the router even with off. They have to: the manager's own pages, the moderation menu item and the dashboard widget call /mgr/pb/* on every manager page, and handing those to the MODX resource resolver returns an HTML 404 where ExtJS expects JSON — which takes the manager down, not just one grid.
Two more settings, both off by default:
| Setting | Default | What it does |
|---|---|---|
pageblocks_route_cache | false | Caches the compiled FastRoute dispatcher. The cache is dropped by itself when a route file is newer than it. |
pageblocks_strict_routes | false | Makes a URL that route() cannot build throw instead of only logging — for development. |
⚙️ Route Methods
Each route is associated with an HTTP method—the way a browser or client sends a request to the server. Here are the main methods:
📄 Route::get($uri, $callback)
Responds to GET requests—when a user simply opens a page.
Route::get('/contact', function () {
return 'Contact information';
});📩 Route::post($uri, $callback)
Handles POST requests—commonly used when submitting forms.
Route::post('/feedback', function () {
return 'Thank you for your feedback!';
});📝 Route::put($uri, $callback)
Used for full resource updates.
Route::put('/profile', function () {
return 'Profile updated';
});🔧 Route::patch($uri, $callback)
Partial resource updates—for example, changing a single field.
Route::patch('/profile/email', function () {
return 'Email updated';
});🗑 Route::delete($uri, $callback)
Deletes a resource.
Route::delete('/account', function () {
return 'Account deleted';
});⚙️ Route::options($uri, $callback)
Handles OPTIONS requests—useful for CORS and APIs.
Route::options('/api/data', function () {
return response('', 204)
->header('Allow', 'GET, POST, OPTIONS')
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
});🔀 Route::match(array $methods, string $uri, $callback)
Defines a route that responds to multiple methods.
For example, if the same URL should handle both GET and POST:
Route::match(['GET', 'POST'], '/subscribe', function () {
return 'Subscription form or its processing';
});👆 This route will work for:
- Opening
/subscribein a browser (GET); - Submitting a form to the same URL (POST).
Methods must be in uppercase (GET, POST, PUT, etc.).
🧮 Route::any(string $uri, $callback)
Responds to all HTTP request types: GET, POST, PUT, DELETE, PATCH, OPTIONS.
Route::any('/ping', function () {
return 'pong';
});This route will work regardless of the client’s method—useful if the request type doesn’t matter.
🧱 Route::crud(string $uri, $controller)
Declares a whole set of routes for one controller in a single line:
Route::crud('mgr/pb/constructor/blocks', BlockController::class);That expands to eleven routes:
| Method | URI | Action |
|---|---|---|
GET | {uri} | index |
POST | {uri} | create |
GET | {uri}/{id} | show — id constrained to [0-9]+ |
POST | {uri}/{id} | update — same constraint |
POST | {uri}/remove | remove |
POST | {uri}/restore | restore |
POST | {uri}/delete | delete |
POST | {uri}/enable | enable |
POST | {uri}/disable | disable |
POST | {uri}/copy | copy |
POST | {uri}/sort | sort |
The numeric constraint on {id} is what keeps {uri}/remove from being swallowed by {uri}/{id}.
Middleware applies to all eleven
Route::crud('mgr/pb/constructor/blocks', BlockController::class)
->middleware(['modauth', 'SudoOnly']);This is the whole point of the helper
A chained ->middleware() after crud() attaches to every route in the batch, not only to the last one declared.
It has to work that way. The alternative — the guard landing on one route out of eleven — does not fail loudly: the listed endpoint still asks for auth, so the set looks secured, while POST {uri}/delete sits wide open next to it. Declaring a CRUD set and guarding it are one action, so they are one call.
->withoutMiddleware() works over the batch in the same way, if you need to lift something the enclosing group applied.
🔁 Redirect Routes
Sometimes, instead of processing a request, you just need to redirect the user to another URL. Use the redirect method for this.
🔁 Route::redirect($from, $to, $status = 301)
Route::redirect('/old-link', '/new-link');👆 Anyone who opens /old-link will be automatically redirected to /new-link.
By default, this uses status code 301 (permanent redirect), but you can specify another:
Route::redirect('/moved', '/here', 302); // temporary redirectOnly the first declaration for a path wins — a second
redirect()to the same path is a duplicate static route, and FastRoute fails on the whole set rather than on that one route.
📦 Useful for site reorganization or URL renaming.
🖼 Quick Route for Displaying a Template
If you just need to display a template without logic, use the Route::view method.
🖼 Route::view($uri, $view, $data = [])
Route::view('/about', 'pages/about');👆 This route will render the pages/about.tpl template.
You can also pass data:
Route::view('/thanks', 'pages/thanks', ['title' => 'Thank you for your order']);Internally, this method calls
response()->view(...), so the result is identical to manually returning a template.
🧩 Route Parameters
Routes can contain variables—parameters that are automatically extracted from the URL and passed to the handler.
🎯 Required Parameters
Route::get('/user/{id}', function ($id) {
return "User profile #$id";
});👆 Visiting /user/42 will output: User profile #42.
❔ Optional Parameters
To make a parameter optional, add ? and specify a default value:
Route::get('/user/{name?}', function ($name = 'Guest') {
return "Hello, $name!";
});📥 /user → Hello, Guest!
📥 /user/Anna → Hello, Anna!
🔍 Parameter Constraints — where()
Allows you to set regular expressions to validate parameters:
Route::get('/user/{id}', function ($id) {
return "User #$id";
})->where('id', '\\d+');👆 Now /user/abc won’t work—only digits (\d+).
Constraints can be set for multiple parameters:
Route::get('/post/{slug}/{id}', function ($slug, $id) {
return "$slug: $id";
})->where([
'slug' => '[a-z\-]+',
'id' => '\\d+',
]);📦 Accessing Request Inside a Route
To access request data, use dependency injection with Request:
use Boshnik\PageBlocks\Http\Request;
Route::post('/submit', function (Request $request) {
$email = $request->input('email');
return "You entered: $email";
});🧃 Named Routes
Named routes are useful for generating links and redirects:
Route::get('/profile/{id}', function ($id) {
return "Profile #$id";
})->name('profile.show');Now you can generate a link to this route:
route('profile.show', ['id' => 7]); // /profile/7🧭 This is especially helpful when generating links in templates or using redirects:
response()->redirect(route('profile.show', ['id' => $user->id]));📦 Route Groups
The group() method lets you group routes and apply shared settings: prefix, middleware, controller, etc.
🔹 Basic Example
Route::group(function () {
Route::get('/a', fn() => 'A');
Route::get('/b', fn() => 'B');
});Routes inside the group will be registered normally—without a prefix or additional parameters.
🏁 Prefix
Adds a common prefix to all routes in the group:
Route::prefix('/admin')->group(function () {
Route::get('/dashboard', fn() => 'Dashboard');
Route::get('/users', fn() => 'Users');
});📥 Registered routes:
/admin/dashboard/admin/users
🛡 Shared Middleware
Route::middleware('auth')->group(function () {
Route::get('/cabinet', fn() => 'Personal account');
});All routes in the group will be processed via the auth middleware.
👨🏫 Default Controller
Sets a shared controller for routes in the group:
Route::controller(ProfileController::class)->group(function () {
Route::get('/edit', 'edit');
Route::post('/update', 'update');
});🔁 Calls the methods:
ProfileController->edit()ProfileController->update()
🛡 Middleware
Middleware are intermediate handlers that run before the controller or closure executes. Examples include auth checks, CSRF protection, etc.
📌 Usage Example
Route::get('/dashboard', fn() => 'Dashboard')->middleware('auth');🧩 Multiple Middleware
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/admin', fn() => 'Admin');
});🚫 Excluding Middleware
You can disable middleware applied earlier:
Route::get('/open', fn() => 'Open')->withoutMiddleware('auth');🧾 Where Are Aliases Configured?
Middleware can be referenced by names (aliases) or by bare class names. Aliases are configured in core/App/bootstrap/app.php:
'middleware' => [
'auth' => Authenticate::class,
],⚠️ That file deliberately has no namespace and no use, so Authenticate::class yields the plain string 'Authenticate' — the runtime prefixes it itself. A fully qualified name there (or passed to ->middleware()) is not found after prefixing, and every route using it answers 403. See Middleware.
🛑 Fallback Routes
If no route matches, you can define a fallback route that runs as a last resort.
🚨 Route::fallback($callback)
Route::fallback(function () {
return response()->view('errors/404')->send();
});📌 This route only triggers if no other route matches.
✅ Useful for custom 404 pages or other global handlers.
Always place fallback routes at the end of all other routes, or they might "catch" unintended requests.
🔐 CSRF Protection
All routes with POST, PUT, PATCH, or DELETE methods are automatically protected against CSRF attacks.
To pass the check, forms must include a hidden token field.
✅ Method 1: {csrf()}
You can use a convenient function in templates:
<form method="POST">
{csrf()}
<button>Submit</button>
</form>This generates:
<input type="hidden" name="_token" value="{csrf_token}">Also supports spoofing if _method is manually added:
<form method="POST">
{csrf()}
<input type="hidden" name="_method" value="PUT">
<button>Update</button>
</form>🛠 Method 2: Manually Insert the Token
You can manually add the hidden field:
<input type="hidden" name="_token" value="{csrf_token}">Useful if you want full control over the HTML.
⚙️ Method 3: Meta Tag with Token
If you use pbFetch, it automatically looks for the CSRF token in a meta tag:
<meta name="csrf-token" content="{csrf_token}">Then, pbFetch will include this token in the X-CSRF-TOKEN header when sending requests.
🔐 Token validation is handled by the VerifyCsrfToken middleware (alias csrf), which is automatically applied to all POST, PUT, PATCH, and DELETE routes outside the mgr context.
If the token is missing or invalid, an AJAX request gets JSON with status 403, and an ordinary form post is redirected back with an error message — see CSRF.