Warning
This feature is still experimental in core. So this is more of a placeholder for future versions of toolkit to support this once it is more ironed out in core.
Summary
WordPress core's @wordpress/build package introduces a powerful file-based routing system for admin pages. This system allows developers to create admin pages and routes through conventional directory structures, with automatic PHP registration and JavaScript bootstrapping handled by the build tool.
10up-toolkit should explore adopting this pattern to simplify admin page development in WordPress plugins.
How It Works in @wordpress/build
1. Declaring Pages in package.json
Admin pages are declared in the root package.json via wpPlugin.pages:
{
"wpPlugin" : {
"pages" : [
" my-settings" ,
{
"id" : " my-dashboard" ,
"init" : [" @my-plugin/dashboard-init" ]
}
]
}
}
Page configuration options:
Format
Example
Description
String
"my-settings"
Simple page with no init modules
Object
{ "id": "my-dashboard", "init": [...] }
Page with initialization modules
2. File-Based Routes
Routes are organized in a routes/ directory at the repository root:
routes/
├── home/
│ ├── package.json # Route configuration
│ ├── stage.tsx # Main content component (required)
│ ├── inspector.tsx # Sidebar component (optional)
│ ├── canvas.tsx # Custom canvas component (optional)
│ └── route.tsx # Lifecycle hooks (optional)
├── settings/
│ ├── package.json
│ └── stage.tsx
└── settings/general/
├── package.json
└── stage.tsx
3. Route Configuration
Each route's package.json defines its path and associated page:
{
"route" : {
"path" : " /" ,
"page" : " my-settings"
}
}
For routes that appear on multiple pages:
{
"route" : {
"path" : " /settings" ,
"page" : [" my-settings" , " my-dashboard" ]
}
}
4. Route Components
File
Purpose
Required
stage.tsx
Main content area
Yes
inspector.tsx
Sidebar/inspector panel
No
canvas.tsx
Custom full-screen canvas (like the block editor)
No
route.tsx
Lifecycle hooks for data loading, auth, etc.
No
Example stage.tsx
export default function SettingsStage ( ) {
return (
< div >
< h1 > Settings</ h1 >
< p > Configure your plugin settings here.</ p >
</ div >
) ;
}
Example inspector.tsx
export default function SettingsInspector ( ) {
return (
< div >
< h2 > Help</ h2 >
< p > Need assistance? Check the documentation.</ p >
</ div >
) ;
}
5. Route Lifecycle Hooks
The route.tsx file exports lifecycle hooks:
export const route = {
// Pre-navigation validation, auth checks
beforeLoad : async ( { params, search } ) => {
const hasPermission = await checkUserPermission ( ) ;
if ( ! hasPermission ) {
throw redirect ( '/unauthorized' ) ;
}
} ,
// Data preloading
loader : async ( { params, search } ) => {
const settings = await fetchSettings ( ) ;
return { settings } ;
} ,
// Canvas control (for editor-like experiences)
canvas : ( { params, search } ) => {
// Return CanvasData to render WordPress editor canvas
// return { postType: 'post', postId: params.id };
// Return null to use custom canvas.tsx
// return null;
// Return undefined for no canvas
return undefined ;
}
} ;
6. Init Modules
Init modules execute during page initialization, before routes are registered. They're useful for:
Adding icons to menu items (icons can't be passed from PHP)
Registering command palette entries
Setting up global state
// packages/my-dashboard-init/src/index.ts
import { dispatch } from '@wordpress/data' ;
import { bootStore } from '@wordpress/boot' ;
import { home , settings } from '@wordpress/icons' ;
export function init ( ) {
dispatch ( bootStore ) . updateMenuItem ( 'home' , { icon : home } ) ;
dispatch ( bootStore ) . updateMenuItem ( 'settings' , { icon : settings } ) ;
}
7. Build Output
The build system generates:
build/
├── pages/
│ └── my-settings/
│ ├── page.php # Full-page mode (custom sidebar)
│ └── page-wp-admin.php # WP-Admin mode (standard interface)
├── routes/
│ ├── home/
│ │ ├── content.js # Bundled stage/inspector/canvas
│ │ └── route.js # Bundled lifecycle hooks
│ ├── settings/
│ │ └── content.js
│ └── index.php # Route registry
├── pages.php # Page registration
└── index.php # Main loader
8. Two Rendering Modes
Full-page mode (page.php):
Takes over the entire admin screen
Custom sidebar with route navigation
Clean, modern interface
WP-Admin mode (page-wp-admin.php):
Integrates within standard WordPress admin
Keeps WordPress admin menu/header
Routes via p query parameter: admin.php?page=my-settings-wp-admin&p=/settings/general
9. PHP Registration
Include the generated PHP in your plugin:
<?php
/**
* Plugin Name: My Plugin
*/
require_once plugin_dir_path ( __FILE__ ) . 'build/index.php ' ;
// Register menu items
add_action ( 'admin_menu ' , function () {
add_menu_page (
'My Settings ' ,
'My Settings ' ,
'manage_options ' ,
'my-settings ' , // Matches wpPlugin.pages ID
'my_settings_render_page ' , // Generated function
'dashicons-admin-generic ' ,
30
);
});
🔮 Proposal for 10up-toolkit
1. File-Based Route Discovery
Support a conventional routes/ directory structure:
src/
├── routes/
│ ├── dashboard/
│ │ ├── route.json # Route configuration
│ │ ├── Stage.tsx # Main content
│ │ └── Inspector.tsx # Optional sidebar
│ └── settings/
│ ├── route.json
│ └── Stage.tsx
└── admin-pages.json # Page definitions
2. Configuration
Define pages in a dedicated config file or 10up-toolkit.config.js:
// 10up-toolkit.config.js
module . exports = {
adminPages : {
enabled : true ,
pages : [
{
id : 'my-plugin-settings' ,
menuTitle : 'My Plugin' ,
capability : 'manage_options' ,
icon : 'dashicons-admin-generic' ,
position : 30
}
] ,
// Output directory for generated PHP
output : 'build/admin'
}
} ;
3. Generated PHP Registration
Generate PHP that handles:
Menu registration via add_menu_page() / add_submenu_page()
Script/style enqueueing
Route data passing to JavaScript
Capability checks
<?php
// build/admin/pages.php (generated)
function tenup_my_plugin_settings_register_page () {
add_menu_page (
__ ( 'My Plugin ' , 'my-plugin ' ),
__ ( 'My Plugin ' , 'my-plugin ' ),
'manage_options ' ,
'my-plugin-settings ' ,
'tenup_my_plugin_settings_render ' ,
'dashicons-admin-generic ' ,
30
);
}
add_action ( 'admin_menu ' , 'tenup_my_plugin_settings_register_page ' );
function tenup_my_plugin_settings_render () {
wp_enqueue_script ( 'my-plugin-settings-page ' );
wp_enqueue_style ( 'my-plugin-settings-page ' );
echo '<div id="my-plugin-settings-root"></div> ' ;
}
4. JavaScript Bootstrap
Generate a bootstrap script that:
Mounts the React app
Sets up routing (React Router, TanStack Router, etc.)
Loads route components dynamically
// build/admin/bootstrap.tsx (generated)
import { createRoot } from 'react-dom/client' ;
import { RouterProvider } from '@tanstack/react-router' ;
import { router } from './router' ;
const container = document . getElementById ( 'my-plugin-settings-root' ) ;
if ( container ) {
const root = createRoot ( container ) ;
root . render ( < RouterProvider router = { router } /> ) ;
}
Benefits
Convention over configuration : Drop files in the right place, they become routes
Reduced boilerplate : No manual menu registration, script enqueueing, or render functions
Modern DX : File-based routing familiar to Next.js/Remix developers
Type safety : Full TypeScript support for route params and loader data
Code splitting : Routes can be lazy-loaded for better performance
Alignment with core : Follows patterns WordPress core is adopting
Implementation Considerations
Router Choice
Should 10up-toolkit:
Bundle a specific router (TanStack Router, React Router)?
Generate router-agnostic code?
Let projects choose their router?
PHP Generation Complexity
The PHP generation needs to handle:
Translatable strings
Different menu types (top-level, submenu, options page)
Capability checks
Multisite considerations
Backward Compatibility
Projects with existing admin pages should be able to:
Migrate incrementally
Mix traditional and file-based pages
Opt out entirely
Questions to Consider
Should this be a separate package or built into 10up-toolkit core?
What router library should be used (or should it be configurable)?
How do we handle the inspector/canvas patterns - are they useful outside of Gutenberg-like experiences?
Should we support the init modules pattern, or is that overkill for most plugins?
How do we handle data loading - built-in loader pattern or let projects use React Query/SWR?
References
Warning
This feature is still experimental in core. So this is more of a placeholder for future versions of toolkit to support this once it is more ironed out in core.
Summary
WordPress core's
@wordpress/buildpackage introduces a powerful file-based routing system for admin pages. This system allows developers to create admin pages and routes through conventional directory structures, with automatic PHP registration and JavaScript bootstrapping handled by the build tool.10up-toolkit should explore adopting this pattern to simplify admin page development in WordPress plugins.
How It Works in @wordpress/build
1. Declaring Pages in
package.jsonAdmin pages are declared in the root
package.jsonviawpPlugin.pages:{ "wpPlugin": { "pages": [ "my-settings", { "id": "my-dashboard", "init": ["@my-plugin/dashboard-init"] } ] } }Page configuration options:
"my-settings"{ "id": "my-dashboard", "init": [...] }2. File-Based Routes
Routes are organized in a
routes/directory at the repository root:3. Route Configuration
Each route's
package.jsondefines its path and associated page:{ "route": { "path": "/", "page": "my-settings" } }For routes that appear on multiple pages:
{ "route": { "path": "/settings", "page": ["my-settings", "my-dashboard"] } }4. Route Components
stage.tsxinspector.tsxcanvas.tsxroute.tsxExample
stage.tsxExample
inspector.tsx5. Route Lifecycle Hooks
The
route.tsxfile exports lifecycle hooks:6. Init Modules
Init modules execute during page initialization, before routes are registered. They're useful for:
7. Build Output
The build system generates:
8. Two Rendering Modes
Full-page mode (
page.php):WP-Admin mode (
page-wp-admin.php):pquery parameter:admin.php?page=my-settings-wp-admin&p=/settings/general9. PHP Registration
Include the generated PHP in your plugin:
🔮 Proposal for 10up-toolkit
1. File-Based Route Discovery
Support a conventional
routes/directory structure:2. Configuration
Define pages in a dedicated config file or
10up-toolkit.config.js:3. Generated PHP Registration
Generate PHP that handles:
add_menu_page()/add_submenu_page()4. JavaScript Bootstrap
Generate a bootstrap script that:
Benefits
Implementation Considerations
Router Choice
Should 10up-toolkit:
PHP Generation Complexity
The PHP generation needs to handle:
Backward Compatibility
Projects with existing admin pages should be able to:
Questions to Consider
References