Files
backstage/subpage-implementation-summary.md
T
Patrik Oldsberg 4b996d05d3 frontend-plugin-api: initial support for subpages
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
2026-02-17 11:55:22 +01:00

8.0 KiB

SubPageBlueprint Implementation Summary

What Was Implemented

I've successfully implemented the SubPageBlueprint pattern for the header architecture, addressing one of the critical gaps identified in the RFC alignment analysis.

Files Created/Modified

New Files

  1. packages/frontend-plugin-api/src/blueprints/SubPageBlueprint.tsx
    • New blueprint for creating sub-pages that attach to parent pages
    • Outputs: routePath, reactElement, title
    • Supports lazy loading of sub-page content
    • Requires users to specify attachTo to target their parent page

Modified Files

  1. packages/frontend-plugin-api/src/blueprints/index.ts

    • Exported SubPageBlueprint for public use
  2. packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx

    • Enhanced to handle sub-pages via inputs.pages
    • Three rendering modes:
      • With loader: Renders Header + lazy-loaded content
      • With sub-pages: Renders Header with tabs + nested Routes for sub-pages
      • Empty: Renders just Header
    • Removed duplicate header rendering issues
  3. plugins/app/src/extensions/AppRoutes.tsx

    • Simplified to focus on top-level routing only
    • Removed duplicate header orchestration logic
    • Removed debug console.log statements
    • Sub-page routing is now handled by PageBlueprint
  4. plugins/app-visualizer/src/plugin.tsx

    • Added import for SubPageBlueprint
    • Cleaned up unused SubRouteRef declarations
    • Now properly uses SubPageBlueprint pattern
  5. plugins/api-docs/src/alpha.tsx

    • Removed HeaderActionBlueprint usage (feature temporarily removed)
    • Cleaned up unused imports
  6. plugins/catalog/src/alpha/plugin.tsx

    • Removed display property (not yet supported in plugin options)

Architecture Overview

SubPageBlueprint API

const mySubPage = SubPageBlueprint.make({
  attachTo: { id: 'page:my-plugin', input: 'pages' },
  name: 'overview',
  params: {
    path: '/overview', // Relative path from parent
    title: 'Overview', // Tab title
    loader: () => import('./components/Overview').then(m => <m.Overview />),
  },
});

How It Works

  1. Parent Page Declaration (without loader, with sub-pages):

    const parentPage = PageBlueprint.make({
      params: {
        path: '/visualizer',
        routeRef: rootRouteRef,
        title: 'Visualizer',
        // No loader - will show tabs
      },
    });
    
  2. Sub-Page Declarations:

    const treePage = SubPageBlueprint.make({
      attachTo: { id: 'page:app-visualizer', input: 'pages' },
      name: 'tree',
      params: {
        path: '/tree',
        title: 'Tree',
        loader: () => import('./TreeView'),
      },
    });
    
  3. Rendering Flow:

    AppRoutes
      └─> PageBlueprint (detects sub-pages via inputs.pages)
            ├─> Header (with tabs)
            └─> Routes
                  ├─> /tree -> TreePage content
                  ├─> /details -> DetailsPage content
                  └─> /text -> TextPage content
    

Key Design Decisions

1. Simplified AppRoutes

  • Removed header orchestration from AppRoutes
  • AppRoutes now only handles top-level routing
  • PageBlueprint is responsible for its own header rendering

2. No Header Duplication

  • Pages with loaders: Header rendered by PageBlueprint
  • Pages with sub-pages: Header with tabs rendered by PageBlueprint
  • No duplicate headers between AppRoutes and PageBlueprint

3. Plugin-Relative Attachment

  • Sub-pages attach using: { id: 'page:plugin-id', input: 'pages' }
  • Follows the pattern described in header.md and the RFC

4. Lazy Loading

  • All sub-pages use lazy loading via ExtensionBoundary.lazy()
  • Improves initial page load performance

5. Removed Features (Temporarily)

  • HeaderActionBlueprint: Removed from AppRoutes orchestration (needs redesign)
  • Plugin display metadata: Removed (not yet supported in plugin options type)
  • SubRouteRef support: Simplified to only support RouteRef

Example Usage: App Visualizer

The app-visualizer plugin demonstrates the complete pattern:

// Parent page without loader
const appVisualizerPage = PageBlueprint.make({
  params: {
    path: '/visualizer',
    routeRef: rootRouteRef,
    title: 'Visualizer',
  },
});

// Three sub-pages attached to parent
const appVisualizerTreePage = SubPageBlueprint.make({
  attachTo: { id: 'page:app-visualizer', input: 'pages' },
  name: 'tree',
  params: {
    path: '/tree',
    title: 'Tree',
    loader: () =>
      import('./components/AppVisualizerPage/TreeVisualizer').then(m => {
        const Component = () => {
          const appTreeApi = useApi(appTreeApiRef);
          const { tree } = appTreeApi.getTree();
          return <m.TreeVisualizer tree={tree} />;
        };
        return <Component />;
      }),
  },
});

// Similar for details and text pages...

Technical Benefits

  1. Declarative: Sub-pages are extensions that can be discovered and manipulated
  2. Type-Safe: Full TypeScript support with proper type checking
  3. Consistent: All pages get headers automatically
  4. Flexible: Supports both simple pages and complex tabbed interfaces
  5. Performant: Lazy loading of sub-page content

What's Still Missing

  1. HeaderActionBlueprint Integration: Needs to be redesigned to work without AppRoutes orchestration
  2. Plugin Display Metadata: display: { icon, title } type support in plugin options
  3. SubRouteRef Support: Currently only RouteRef is supported for routing
  4. Sidebar Navigation: The RFC's primary goal for Portal (sidebar navigation API)
  5. Breadcrumbs: Mentioned in RFC but not implemented

Testing Recommendations

  1. Test sub-page navigation: Click through tabs in app-visualizer
  2. Test lazy loading: Verify sub-pages load on demand
  3. Test routing: Verify URLs update correctly when switching tabs
  4. Test nested routes: Verify sub-pages render at correct paths
  5. Test simple pages: Verify pages with loaders still work

Migration Path for Other Plugins

To add sub-pages to an existing plugin:

  1. Update parent page to remove loader:

    const myPage = PageBlueprint.make({
      params: {
        path: '/my-plugin',
        title: 'My Plugin',
        // Remove loader to enable sub-pages
      },
    });
    
  2. Create sub-pages:

    const mySubPage = SubPageBlueprint.make({
      attachTo: { id: 'page:my-plugin', input: 'pages' },
      name: 'overview',
      params: {
        path: '/overview',
        title: 'Overview',
        loader: () => import('./Overview'),
      },
    });
    
  3. Add to plugin extensions:

    export default createFrontendPlugin({
      extensions: [
        myPage,
        mySubPage,
        // ... other sub-pages
      ],
    });
    

Type Safety

All type errors have been resolved:

  • No unused imports
  • Proper type checking for all parameters
  • Correct extension data types
  • Valid blueprint definitions

Next Steps

  1. Add tests for SubPageBlueprint and updated PageBlueprint
  2. Implement HeaderActionBlueprint redesign (perhaps as page-level extensions)
  3. Add plugin display metadata support in plugin options types
  4. Implement sidebar navigation API (RFC primary goal)
  5. Add breadcrumb support for navigation hierarchy
  6. Document the pattern for community plugin authors
  7. Update existing plugins to use the new pattern

Conclusion

The SubPageBlueprint implementation successfully delivers on the core technical requirements from the RFC:

  • Sub-pages are represented as extensions
  • Sub-pages have titles and relative paths
  • Sub-pages attach to parent pages
  • Two levels of navigation (page + sub-pages)
  • Consistent headers across plugins
  • Plugin-relative attachment points

The implementation is type-safe, follows Backstage patterns, and provides a clean API for plugin authors to create rich, multi-page experiences within their plugins.