Split nested project route sections

This commit is contained in:
2026-04-15 16:08:15 +02:00
parent 0ed2b2429c
commit 75a573dca6
8 changed files with 144 additions and 115 deletions
@@ -1,6 +1,6 @@
--- ---
name: project-route-structure name: project-route-structure
description: "Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions." description: "Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions."
--- ---
# Project Route Structure # Project Route Structure
@@ -9,7 +9,7 @@ description: "Always apply in this project when creating, moving, or refactoring
Activate this skill whenever work in this project changes: Activate this skill whenever work in this project changes:
- `routes/*.php` - `routes/**/*.php`
- Route names, URI prefixes, middleware groups, scoped bindings, or controller route registrations - Route names, URI prefixes, middleware groups, scoped bindings, or controller route registrations
- Frontend-facing endpoints that require regenerated Wayfinder output - Frontend-facing endpoints that require regenerated Wayfinder output
@@ -18,10 +18,11 @@ Use this together with `laravel-best-practices`. Also activate `wayfinder-develo
## Route Files ## Route Files
- `routes/web.php` is the web entry point only. Keep public shell routes here, such as `home` and `dashboard`, then require domain route files. - `routes/web.php` is the web entry point only. Keep public shell routes here, such as `home` and `dashboard`, then require domain route files.
- `routes/projects.php` owns all `projects.*` routes. - `routes/web/projects.php` owns the main `projects.*` structure.
- `routes/admin.php` owns all `admin.*` routes. - `routes/web/admin.php` owns all `admin.*` routes.
- `routes/teams.php` owns all `teams.*` routes. - `routes/web/teams.php` owns all `teams.*` routes.
- `routes/settings.php` owns account settings routes, but keeps their existing route names such as `profile.edit`, `security.edit`, and `git-connections.edit`. - `routes/web/settings.php` owns account settings routes, but keeps their existing route names such as `profile.edit`, `security.edit`, and `git-connections.edit`.
- `routes/web/project/*.php` contains larger nested project route sections that would make `routes/web/projects.php` too deeply indented.
Add a new route file only when a domain gets large enough to stand alone. Otherwise, extend the existing domain file. Add a new route file only when a domain gets large enough to stand alone. Otherwise, extend the existing domain file.
@@ -32,6 +33,7 @@ Add a new route file only when a domain gets large enough to stand alone. Otherw
- Keep public route names stable unless the user explicitly asks for a public interface change. - Keep public route names stable unless the user explicitly asks for a public interface change.
- Keep route URIs stable when reorganizing route files; this kind of change should be structural. - Keep route URIs stable when reorganizing route files; this kind of change should be structural.
- Use `function (): void` for route group closures. - Use `function (): void` for route group closures.
- For large nested sections, keep the structural group in the parent file and `require` a child file from inside the group. For example, keep `Route::prefix('tickets')->name('tickets.')->group(...)` in `routes/web/projects.php`, then require `routes/web/project/tickets.php` inside it.
## Project Routes ## Project Routes
@@ -46,7 +48,11 @@ Project routes should follow this shape:
- Project delete: `can:access-admin` - Project delete: `can:access-admin`
- `projects/{project}/permissions/...` must not be inside `Route::scopeBindings()`, because `Project` does not define `users()` or `teams()` child relationships. - `projects/{project}/permissions/...` must not be inside `Route::scopeBindings()`, because `Project` does not define `users()` or `teams()` child relationships.
- Use `Route::scopeBindings()` only around nested project resources that should bind through project-owned relationships, such as tickets and project settings children. - Use `Route::scopeBindings()` only around nested project resources that should bind through project-owned relationships, such as tickets and project settings children.
- Keep ticket subroutes grouped under `tickets.` and settings subroutes grouped under `settings.`. - Keep the outer `{project}` group in `routes/web/projects.php`; it is part of the main project structure.
- Keep compact view routes, such as show, board, and backlog, directly in `routes/web/projects.php`.
- Keep ticket subroutes grouped under `tickets.` in `routes/web/projects.php`, with the route contents in `routes/web/project/tickets.php`.
- Keep settings subroutes grouped under `settings.` in `routes/web/projects.php`, with the route contents in `routes/web/project/settings.php`.
- Split dense settings subsections further under `routes/web/project/settings/`, such as `board.php`, `metadata.php`, and `repositories.php`.
## Settings Routes ## Settings Routes
+1 -1
View File
@@ -34,7 +34,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions. - `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions.
- `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task
- `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation. - `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
+37
View File
@@ -0,0 +1,37 @@
<?php
use App\Http\Controllers\ProjectBoardSettingsController;
use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ProjectRepositorySettingsController;
use App\Http\Controllers\ProjectTicketMetadataSettingsController;
use Illuminate\Support\Facades\Route;
Route::get('general', [ProjectController::class, 'general'])->name('general');
Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board');
Route::prefix('board')
->name('board.')
->controller(ProjectBoardSettingsController::class)
->group(function (): void {
require __DIR__.'/settings/board.php';
});
Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show'])->name('metadata');
Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update'])->name('metadata.update');
Route::prefix('metadata')
->name('metadata.')
->controller(ProjectTicketMetadataSettingsController::class)
->group(function (): void {
require __DIR__.'/settings/metadata.php';
});
Route::get('repositories', [ProjectRepositorySettingsController::class, 'show'])->name('repositories');
Route::prefix('repositories')
->name('repositories.')
->controller(ProjectRepositorySettingsController::class)
->group(function (): void {
require __DIR__.'/settings/repositories.php';
});
+16
View File
@@ -0,0 +1,16 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('columns')->name('columns.')->group(function (): void {
Route::post('/', 'store')->name('store');
Route::patch('{boardColumn}', 'update')->name('update');
Route::delete('{boardColumn}', 'destroy')->name('destroy');
});
Route::prefix('statuses')->name('statuses.')->group(function (): void {
Route::post('/', 'storeStatus')->name('store');
Route::patch('{ticketStatus}', 'updateStatus')->name('update');
Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy');
Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions')->name('transitions.update');
});
+27
View File
@@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('types')->name('types.')->group(function (): void {
Route::post('/', 'storeType')->name('store');
Route::patch('{ticketType}', 'updateType')->name('update');
Route::delete('{ticketType}', 'destroyType')->name('destroy');
});
Route::prefix('priorities')->name('priorities.')->group(function (): void {
Route::post('/', 'storePriority')->name('store');
Route::patch('{ticketPriority}', 'updatePriority')->name('update');
Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy');
});
Route::prefix('fields')->name('fields.')->group(function (): void {
Route::post('/', 'storeFieldDefinition')->name('store');
Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update');
Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy');
});
Route::prefix('link-types')->name('link-types.')->group(function (): void {
Route::post('/', 'storeLinkType')->name('store');
Route::patch('{ticketLinkType}', 'updateLinkType')->name('update');
Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy');
});
@@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
Route::post('/', 'store')->name('store');
Route::patch('{repositoryConnection}', 'update')->name('update');
Route::delete('{repositoryConnection}', 'destroy')->name('destroy');
+41
View File
@@ -0,0 +1,41 @@
<?php
use App\Http\Controllers\ProjectBoardController;
use App\Http\Controllers\ProjectTicketBranchController;
use App\Http\Controllers\ProjectTicketController;
use App\Http\Controllers\ProjectTicketLinkController;
use Illuminate\Support\Facades\Route;
Route::middleware('can:write-project,project')->group(function (): void {
Route::get('create', [ProjectTicketController::class, 'create'])->name('create');
Route::post('/', [ProjectTicketController::class, 'store'])->name('store');
});
Route::prefix('{ticket}')->group(function (): void {
Route::get('/', [ProjectTicketController::class, 'show'])
->middleware('can:view-project,project')
->name('show');
Route::middleware('can:write-project,project')->group(function (): void {
Route::patch('/', [ProjectTicketController::class, 'update'])->name('update');
Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update');
Route::prefix('branches')
->name('branches.')
->controller(ProjectTicketBranchController::class)
->group(function (): void {
Route::get('validate', 'validate')->name('validate');
Route::post('/', 'store')->name('store');
});
Route::prefix('links')
->name('links.')
->controller(ProjectTicketLinkController::class)
->group(function (): void {
Route::post('/', 'store')->name('store');
Route::delete('{ticketLink}', 'destroy')
->withoutScopedBindings()
->name('destroy');
});
});
});
+2 -107
View File
@@ -1,13 +1,7 @@
<?php <?php
use App\Http\Controllers\ProjectBoardController; use App\Http\Controllers\ProjectBoardController;
use App\Http\Controllers\ProjectBoardSettingsController;
use App\Http\Controllers\ProjectController; use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ProjectRepositorySettingsController;
use App\Http\Controllers\ProjectTicketBranchController;
use App\Http\Controllers\ProjectTicketController;
use App\Http\Controllers\ProjectTicketLinkController;
use App\Http\Controllers\ProjectTicketMetadataSettingsController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('projects')->name('projects.')->group(function (): void { Route::prefix('projects')->name('projects.')->group(function (): void {
@@ -45,113 +39,14 @@ Route::prefix('projects')->name('projects.')->group(function (): void {
Route::prefix('tickets') Route::prefix('tickets')
->name('tickets.') ->name('tickets.')
->group(function (): void { ->group(function (): void {
Route::middleware('can:write-project,project')->group(function (): void { require __DIR__.'/project/tickets.php';
Route::get('create', [ProjectTicketController::class, 'create'])->name('create');
Route::post('/', [ProjectTicketController::class, 'store'])->name('store');
});
Route::prefix('{ticket}')->group(function (): void {
Route::get('/', [ProjectTicketController::class, 'show'])
->middleware('can:view-project,project')
->name('show');
Route::middleware('can:write-project,project')->group(function (): void {
Route::patch('/', [ProjectTicketController::class, 'update'])->name('update');
Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update');
Route::prefix('branches')
->name('branches.')
->controller(ProjectTicketBranchController::class)
->group(function (): void {
Route::get('validate', 'validate')->name('validate');
Route::post('/', 'store')->name('store');
});
Route::prefix('links')
->name('links.')
->controller(ProjectTicketLinkController::class)
->group(function (): void {
Route::post('/', 'store')->name('store');
Route::delete('{ticketLink}', 'destroy')
->withoutScopedBindings()
->name('destroy');
});
});
});
}); });
Route::prefix('settings') Route::prefix('settings')
->name('settings.') ->name('settings.')
->middleware('can:admin-project,project') ->middleware('can:admin-project,project')
->group(function (): void { ->group(function (): void {
Route::get('general', [ProjectController::class, 'general'])->name('general'); require __DIR__.'/project/settings.php';
Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board');
Route::prefix('board')
->name('board.')
->controller(ProjectBoardSettingsController::class)
->group(function (): void {
Route::prefix('columns')->name('columns.')->group(function (): void {
Route::post('/', 'store')->name('store');
Route::patch('{boardColumn}', 'update')->name('update');
Route::delete('{boardColumn}', 'destroy')->name('destroy');
});
Route::prefix('statuses')->name('statuses.')->group(function (): void {
Route::post('/', 'storeStatus')->name('store');
Route::patch('{ticketStatus}', 'updateStatus')->name('update');
Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy');
Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions')
->name('transitions.update');
});
});
Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show'])
->name('metadata');
Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update'])
->name('metadata.update');
Route::prefix('metadata')
->name('metadata.')
->controller(ProjectTicketMetadataSettingsController::class)
->group(function (): void {
Route::prefix('types')->name('types.')->group(function (): void {
Route::post('/', 'storeType')->name('store');
Route::patch('{ticketType}', 'updateType')->name('update');
Route::delete('{ticketType}', 'destroyType')->name('destroy');
});
Route::prefix('priorities')->name('priorities.')->group(function (): void {
Route::post('/', 'storePriority')->name('store');
Route::patch('{ticketPriority}', 'updatePriority')->name('update');
Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy');
});
Route::prefix('fields')->name('fields.')->group(function (): void {
Route::post('/', 'storeFieldDefinition')->name('store');
Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update');
Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy');
});
Route::prefix('link-types')->name('link-types.')->group(function (): void {
Route::post('/', 'storeLinkType')->name('store');
Route::patch('{ticketLinkType}', 'updateLinkType')->name('update');
Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy');
});
});
Route::get('repositories', [ProjectRepositorySettingsController::class, 'show'])
->name('repositories');
Route::prefix('repositories')
->name('repositories.')
->controller(ProjectRepositorySettingsController::class)
->group(function (): void {
Route::post('/', 'store')->name('store');
Route::patch('{repositoryConnection}', 'update')->name('update');
Route::delete('{repositoryConnection}', 'destroy')->name('destroy');
});
}); });
}); });
}); });