kusikusicms / routes
Run-time routes for KusikusiCMS entities: slug-based paths, redirects and a catch-all resolver.
Requires
- php: ^8.4
- illuminate/console: ^13.0
- illuminate/database: ^13.0
- illuminate/filesystem: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- kusikusicms/models: ^13.0
- laravel/prompts: ^0.3.25
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-08 21:47:08 UTC
README
Read-only split. This repository is automatically split from
packages/routesin the kusikusicms/monorepo. Please open issues and pull requests in the monorepo; pull requests sent here are closed automatically. Install it withcomposer require kusikusicms/routes.
Run-time routes for KusikusiCMS entities:
- Paths built from entity slugs (
/section/my-page), stored in theentities_routestable. - Kept in sync automatically: when a slug changes, the old path becomes a 301 redirect and every descendant follows.
- A catch-all route that resolves incoming URLs to an entity, model, view, format and language, and hands them to a controller in your app.
Version: 13.x
This package requires kusikusicms/models.
Installation
- Require the package:
composer require kusikusicms/routes
- Run the installer. It asks for the primary domain (default: the
app.urlhost), the language (default:kusikusicms.models.default_language) and the root entity id (default:home):
php artisan kusikusicms:routes:install
It creates config/kusikusicms/routes.php, app/Http/Controllers/WebsiteController.php and resources/views/html/home.blade.php. Existing files are kept unless you pass --force.
- Run the migrations and remove any
/route of your app (for example the defaultwelcomeroute), because app routes win over the catch-all:
php artisan migrate
See docs/installation.md for options and vendor:publish tags.
Configuration
All keys live under kusikusicms.routes.*:
models(default['*']) andexcept_models(default[]): which entity models get routes.slug_field(defaultslug): the content field used to build paths.domains: the site, keyed by host, withroot_entity_id(only its tree gets routes) andlang.catch_all.enabled(defaulttrue) andcatch_all.middleware(default['web']).controller(defaultApp\Http\Controllers\WebsiteController): the host controller.formats(defaulthtml,rss,amp,json,xml) anddefault_format(defaulthtml).
Quick start
use KusikusiCMS\Models\Entity; Entity::create(['id' => 'home', 'model' => 'Home'])->createContents(['title' => 'Home', 'slug' => '']); Entity::create(['id' => 'blog', 'parent_entity_id' => 'home', 'model' => 'Section']) ->createContents(['title' => 'Blog', 'slug' => 'blog']); Entity::create(['id' => 'p1', 'parent_entity_id' => 'blog', 'model' => 'BlogPost']) ->createContents(['title' => 'Hello', 'slug' => 'hello']); // entities_routes now has "/" (home), "/blog" (blog) and "/blog/hello" (p1), all primary. Entity::find('blog')->createContents(['slug' => 'news']); // "/news" and "/news/hello" are the new primary routes; // "/blog" and "/blog/hello" are permanent redirects to them.
Then add a method per model to your WebsiteController:
public function blogPost(Request $request, ResolvedRoute $route): View { $entity = Entity::withContents(['lang' => $route->lang])->findOrFail($route->entityId); return view($route->viewName(), ['entity' => $entity]); // "html.blog_post" }
Route kinds
The kind column is the KusikusiCMS\Routes\Enums\RouteKind enum:
| Kind | Value | Response |
|---|---|---|
Primary |
primary |
The canonical path of an entity in a language (one per entity and language). Rendered by the host controller. |
Alias |
alias |
Another path rendering the same entity. |
PermanentRedirect |
permanent-redirect |
301 to the primary path of the same entity and language. Created automatically when a slug changes. |
TemporaryRedirect |
temporary-redirect |
302 to the primary path of the same entity and language. |
The root entity always gets / in the site language, with no slug content needed. Paths are globally unique: only one entity and one language can use a path. Use an explicit slug for other language roots (for example the home slug es gives /es, /es/blog, ...).
Manual API
use KusikusiCMS\Routes\EntityRoute; use KusikusiCMS\Routes\Enums\RouteKind; EntityRoute::createFor('p1', 'en', '/hi', RouteKind::Alias); EntityRoute::createFor('p1', 'en', '/promo', RouteKind::TemporaryRedirect); $route = EntityRoute::findByPath('/blog/hello'); // or null $route?->entity_id; // "p1" $route?->lang; // "en"
createFor() denormalizes the entity model and view, and throws RoutePathConflictException when another entity (or another language) already uses the path as primary or alias. The service behind it, KusikusiCMS\Routes\Services\RouteManager, is a container singleton you can extend and rebind.
Querying routes
The routes package adds a routes relationship to Entity (a hasMany of EntityRoute, registered when the package boots) and a withRoute() macro to the Entity query builder:
with('routes')eager loads everyEntityRouteof the entity (all languages and kinds, each withpath,langandkind). Constrain it as usual:with(['routes' => fn ($q) => $q->where('lang', 'es')]).withRoute(string|array|null $lang = null)adds arouteattribute with the primary path of the entity in the language (ornull). It accepts an options array such as['lang' => 'es'], likewithContents(). When$langis null,kusikusicms.models.default_languageis used.
$pages = Entity::query()->childrenOf('home')->withRoute('en')->get(); $page = Entity::query()->with('routes')->findOrFail('page-1'); $page->routes->where('lang', 'es')->where('kind', RouteKind::Primary)->first()?->path;
withRoute() loads the paths after the query, so it never changes the selected columns and works with paginate(). Its attribute is not dirty, so save() never writes it. Calling it on a non-entity query throws a LogicException.
IDE support
withRoute() is added to EntityBuilder at run time (mixed in from KusikusiCMS\Routes\Builders\EntityRouteQueries, where Cmd+click lands). To silence the "method not found" warnings in PhpStorm and the Laravel IDE plugin, the package ships vendor/kusikusicms/routes/stubs/ide-helper.php. It is never autoloaded: it only declares @method, @property and @mixin docblocks for EntityBuilder and Entity. PhpStorm indexes the vendor folder by itself; if you use PHPStan/Larastan, do not add the stub to its paths.
Host controller
The catch-all route parses the URL (/blog/hello.rss gives the path /blog/hello and the format rss), finds the route, checks that the entity is live and calls handle() on the configured controller. handle(), inherited from KusikusiCMS\Routes\Http\Controllers\EntityRouteController, calls the public method named after the model in camelCase (Home → home(), BlogPost → blogPost()) or fallback() (404 by default).
Views follow the {format}.{view} convention: $route->viewName() returns html.home, rss.blog_post, etc.
Errors
- Unknown paths and formats, entities that are not live and redirects without a primary respond 404 through
abort(404). A missing view or any other bug is a 500. Your app's exception handler renders them. RoutePathConflictException(withpath,conflictingEntityIdandlang) is thrown when a slug change or a move would reuse a taken path. The content or entity write is rolled back.
See docs/errors.md.
Testing
cd packages/routes
../../vendor/bin/phpunit
See docs/testing.md.
Documentation
- docs/installation.md
- docs/configuration.md
- docs/route-syncing.md
- docs/request-handling.md
- docs/errors.md
- docs/testing.md
- docs/design-decisions.md
License
MIT. See LICENSE.