What it does

scriptor-simple-router lets you answer URLs that are not pages: a /api/users/{id} JSON endpoint, a /webhook/stripe receiver, anything request/response shaped. It runs ahead of Scriptor's page resolver, so a matched route short-circuits the normal page flow; an unmatched request falls through untouched.

Reach for it when you would otherwise be tempted to create a fake Page just to get a URL to execute code. It deliberately stays small: no middleware, no route groups, no per-route DI. If you need those, wrap FastRoute or league/route inside a plugin instead; the Cookbook has that recipe. This plugin is the floor, not the framework.

It is also the artefact built line by line across the Build a Module tutorial, so its source doubles as a teaching example of the activation pattern.

Install

Host install. Not on Packagist, so register the VCS repo, then require:

composer config repositories.scriptor-simple-router \
  vcs https://github.com/bigin/scriptor-simple-router
composer require bigins/scriptor-simple-router:^0.1

Docker. Bake it in with the two build args:

services:
  scriptor:
    build:
      args:
        SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-simple-router"
        SCRIPTOR_PLUGINS:      "bigins/scriptor-simple-router:^0.1"

The plugin auto-registers via installed.json. Unlike a page-resolving plugin, the router needs one activation line at the top of your theme's _ext.php:

if (\Bigins\ScriptorSimpleRouter\Router::handle()) return;

Router::handle() returns true only when a route matched (it has already sent the response); every other request returns false and the theme builds as usual.

Configure

There is no plugins.* config block. Routes are code, declared in a routes.php next to your theme's _ext.php:

use Bigins\ScriptorSimpleRouter\Request;
use Bigins\ScriptorSimpleRouter\Response;
use Bigins\ScriptorSimpleRouter\Router;

$router = Router::instance();

$router->get('/api/users/{id}', fn(Request $req) => Response::json([
    'id' => (int) $req->param('id'),
]));

$router->post('/webhook/stripe', StripeWebhookController::class);

A handler is a Closure, a controller class string (invoked through __invoke), or a [Class, method] array. Path parameters in {braces} arrive via $req->param('name').

Use

The smallest end-to-end check: register the route above, add the activation line to _ext.php, then:

curl -s http://localhost:8090/api/users/42
# {"id":42}

A request that matches no route (/about/, say) never reaches the router's response path, so your normal pages keep working.