Skip to main content

Scaffolder Plugin Extension - Custom Template Cards

Overview

This document describes how we extended the Backstage scaffolder plugin to display version labels from Crossplane XRD metadata on template cards.

Problem Statement

We needed to display openportal.dev/version labels from Crossplane XRD templates directly on the template cards in Backstage's /create page. For example, a template with version "1.0.2" should display as "DNSRecord v1.0.2" in the UI.

Solution Architecture

Approach: Local Plugin Copy with Minimal Modifications

After exploring various approaches, we implemented a solution using a local copy of the scaffolder plugin with minimal modifications:

  1. Local Plugin Copy (plugins/scaffolder/)

    • Full copy of the Backstage scaffolder plugin
    • Renamed to @internal/plugin-scaffolder to avoid conflicts
    • Linked via Yarn workspaces
  2. CustomTemplateCard Component

    • Extracts version from openportal.dev/version label
    • Modifies template title to include version
    • Adds XRD tags for Crossplane templates
  3. Router Integration

    • Modified Router to use CustomTemplateCard as default
    • Fallback pattern ensures all templates use our custom card

Implementation Details

File Structure

plugins/scaffolder/
├── src/
│ ├── components/
│ │ ├── CustomTemplateCard.tsx # New: Version display component
│ │ └── Router/
│ │ └── Router.tsx # Modified: Uses CustomTemplateCard
│ └── internals/
│ └── index.ts # New: Stub for experimental features

Key Changes

1. CustomTemplateCard Component

// plugins/scaffolder/src/components/CustomTemplateCard.tsx
export const CustomTemplateCard = ({ template }) => {
const version = template.metadata.labels?.['openportal.dev/version'];

const modifiedTemplate = {
...template,
metadata: {
...template.metadata,
title: version
? `${template.metadata.title || template.metadata.name} v${version.replace(/^v/, '')}`
: template.metadata.title || template.metadata.name,
},
};

return <TemplateCard template={modifiedTemplate} />;
};

2. Router Modification

// plugins/scaffolder/src/components/Router/Router.tsx
const FinalTemplateCardComponent = TemplateCardComponent || CustomTemplateCard;

3. Package Configuration

  • Updated package name to @internal/plugin-scaffolder
  • Fixed dependency versions to match app requirements
  • Added internals stub for experimental features

Challenges and Learnings

1. Extension System Limitations

Challenge: Backstage's new frontend extension system (v1.42.0) has limitations with PageBlueprint.makeWithOverrides.

Learning: The extension system's inputs/outputs pattern doesn't reliably pass custom components through the scaffolder page blueprint. Direct modification of the plugin was more reliable.

2. Dependency Conflicts

Challenge: Nested dependencies caused "useEntityList must be used within EntityListProvider" errors.

Solution: Added Yarn resolutions to force consistent package versions:

"resolutions": {
"@backstage/plugin-catalog-react": "^1.14.5"
}

Learning: Yarn workspaces can create duplicate React contexts when packages have nested dependencies. Always check for nested node_modules folders.

3. API Blueprint Changes

Challenge: ApiBlueprint structure changed between Backstage versions.

Learning: The scaffolder API requires callback form for parameters:

ApiBlueprint.make({
params: defineParams => defineParams({ /* ... */ })
})

4. Import Path Issues

Challenge: Internal imports failed with "@internal/scaffolder" references.

Solution: Created stub implementations and updated imports to use relative paths.

Maintenance Considerations

Pros of This Approach

  • Full control over template card rendering
  • Minimal changes to core functionality
  • Easy to extend with additional features
  • Clear separation from upstream Backstage

Cons of This Approach

  • Requires maintaining a plugin copy
  • Manual updates when upgrading Backstage
  • Potential for version drift with upstream

Future Improvements

  1. Upstream Contribution: Consider contributing a template card customization API to Backstage
  2. Plugin Isolation: Extract only the necessary components instead of copying the entire plugin
  3. Extension API: Monitor Backstage development for improved extension APIs

Troubleshooting

Common Issues

  1. Build Errors with Missing Exports

    • Check if experimental features need stub implementations
    • Verify all imports use correct paths (relative vs package)
  2. Duplicate React Context Errors

    • Check for nested node_modules: ls node_modules/@backstage/*/node_modules
    • Add resolutions to root package.json
    • Clean and reinstall: rm -rf node_modules && yarn install
  3. Version Mismatch Errors

    • Ensure frontend-plugin-api versions match across packages
    • Check that all Backstage packages use compatible versions

Debug Commands

# Check for nested dependencies
ls node_modules/@backstage/plugin-scaffolder-react/node_modules

# Verify package resolutions
yarn why @backstage/plugin-catalog-react

# Clean Yarn cache and reinstall
rm -rf node_modules/.yarn-state.yml && yarn install

Version Compatibility

Tested with:

  • Backstage: v1.42.0
  • Frontend System: New Frontend System
  • Node.js: v20
  • Yarn: v4.4.1

Summary

The custom scaffolder plugin approach provides a pragmatic solution for displaying version labels on template cards. While it requires maintaining a local plugin copy, it offers complete control over the template card rendering and has proven stable in production use.

The key insight is that sometimes a direct modification is more maintainable than complex workarounds with an evolving extension system. This approach balances customization needs with maintainability concerns.