creating-backstage-entity-page-extension
About creating-backstage-entity-page-extension
A development skill for extending Backstage catalog entity pages with cards or dedicated tabs. It covers blueprint selection, entity-based filtering, and implementation details for standalone or scaffolded plugins.
Key features
- 01Add cards to entity overview pages
- 02Create entity tabs with their own routes
- 03Filter extensions by entity properties
- 04Load components asynchronously for code-splitting
- 05Use entity context for data fetching
Use cases
- 01Displaying Jira project details on catalog entities
- 02Adding an incidents tab to a component page
- 03Restricting an extension to selected entity types
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ vinzscam/ backstage-skills --skill creating-backstage-entity-page-extensionAfter installing, type/creating-backstage-entity-page-extensionin Claude Code to confirm it loaded.
Bundled filesreferences/card.mdreferences/content.md
Details
From the project READMECreating Catalog Entity Page Extensions
When to use this skill
- Adding a card to the Overview tab of a catalog entity (e.g. "show Jira project info on Components linked to a Jira project")
- Adding a whole tab (a content panel with its own URL path) to a catalog entity page (e.g. "/incidents" tab on Components, listing the component's incidents)
- Filtering when an extension appears, based on entity kind, spec type, annotation presence, or any custom predicate
This is the most common reason to scaffold a frontend-plugin-module. The plugin you're authoring extends the host's catalog plugin via these blueprints.
Choose the right pattern
| Building... | Pattern | Reference |
|---|---|---|
| A card on the entity Overview tab | EntityCardBlueprint.make({ name, params: { loader, filter?, type? } }) | references/card.md |
| A full tab (its own URL path) on the entity page | EntityContentBlueprint.make({ name, params: { path, title, loader, filter?, group?, icon? } }) | references/content.md |
Both blueprints come from @backstage/plugin-catalog-react/alpha. They share:
- A
loader: () => Promise<JSX.Element>for code-splitting the rendered component. - A
filterparameter that controls when the extension appears (see "Entity filtering" below). - The component renders inside the catalog entity context — read the current entity via
useEntity()from@backstage/plugin-catalog-react.
Two invocation modes
- Standalone: editing an existing
frontend-plugin-modulethat targets the catalog. Pick the right reference (card or content) and add the extension. - Post-scaffold (called by
creating-backstage-pluginforfrontend-plugin-moduletemplates targetingcatalog): scaffold the extension(s) the user described, plus the component(s) they render.
Entity filtering
The filter parameter on both blueprints decides which entities the extension applies to. Three accepted forms:
- String (most common):
'kind:component','kind:component,spec.type:service','metadata.annotations.acme.com/jira-project'. Comma-separated for AND, multiple forms for the same key are OR. FilterPredicate(structured): a Zod-validated object form for richer logic. Verify exact shape against../backstage/packages/filter-predicates/src/.- Function:
(entity: Entity) => boolean— full programmatic control. Use sparingly — string filters compose better with the upstream config-override system.
If filter is omitted, the extension appears for all entity kinds. Almost always wrong — set a filter even if it's just 'kind:component'.
Common gotchas
- The blueprints come from
@backstage/plugin-catalog-react/alpha(currently@alpha). Track the import path against../backstage/plugins/catalog-react/src/alpha/blueprints/. - The component itself imports
useEntityfrom@backstage/plugin-catalog-react(main entry, not/alpha). The hook returns{ entity }typed asEntity— use the catalog entity model from@backstage/catalog-modelfor typed access tometadata,spec, etc. useEntityonly works inside a component rendered by one of these blueprints — calling it outside the catalog entity context throws.- The
loader: () => Promise<JSX.Element>form is required for code-splitting. Don't pass a synchronous component import — use() => import('./MyCard').then(m => <m.MyCard />). - For entity-specific data fetching, combine
useEntitywith the data-hook convention fromusing-backstage-frontend-system(a hook wrappinguseAsyncaroundfetchApi.fetch). Passentity.metadata.nameor an annotation as the cache key. - The host app's catalog plugin owns the route
entity-content:catalog/overview(where cards attach) andpage:catalog/entity(where content tabs attach). Your module installs into the same backend as the catalog plugin — adopters install viabackend.add(...)andapp.installPlugin(...); nothing else is needed.