Skip to content

Support File-Based Admin Page & Route Registration #476

@fabiankaegy

Description

@fabiankaegy

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

  1. Convention over configuration: Drop files in the right place, they become routes
  2. Reduced boilerplate: No manual menu registration, script enqueueing, or render functions
  3. Modern DX: File-based routing familiar to Next.js/Remix developers
  4. Type safety: Full TypeScript support for route params and loader data
  5. Code splitting: Routes can be lazy-loaded for better performance
  6. 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

  1. Should this be a separate package or built into 10up-toolkit core?
  2. What router library should be used (or should it be configurable)?
  3. How do we handle the inspector/canvas patterns - are they useful outside of Gutenberg-like experiences?
  4. Should we support the init modules pattern, or is that overkill for most plugins?
  5. How do we handle data loading - built-in loader pattern or let projects use React Query/SWR?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions