diff --git a/.tmp/proof/showcase-answer-engine.png b/.tmp/proof/showcase-answer-engine.png
new file mode 100644
index 0000000..a6987de
Binary files /dev/null and b/.tmp/proof/showcase-answer-engine.png differ
diff --git a/.tmp/proof/showcase-full.png b/.tmp/proof/showcase-full.png
new file mode 100644
index 0000000..7fdb82c
Binary files /dev/null and b/.tmp/proof/showcase-full.png differ
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index c1f0a32..ae1f34c 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -53,6 +53,7 @@ import { PrimerDemoPage } from './components/PrimerDemoPage';
import { ScorecardPage } from './components/scorecard';
import { PulseCheckPage } from './components/pulse-check';
import { ExplorePage } from './components/explore';
+import { ShowcasePage } from './components/showcase';
const app = createApp({
apis,
@@ -200,6 +201,7 @@ const routes = (
} />
} />
} />
+ } />
);
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index e6fe8d7..4bf735e 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -19,6 +19,7 @@ import {
ChecklistIcon,
PulseIcon,
TelescopeIcon,
+ StarIcon,
} from '@primer/octicons-react';
import {
Sidebar,
@@ -73,6 +74,7 @@ const RocketOcticon: IconComponent = () => ;
const ChecklistOcticon: IconComponent = () => ;
const PulseOcticon: IconComponent = () => ;
const TelescopeOcticon: IconComponent = () => ;
+const StarOcticon: IconComponent = () => ;
export const Root = ({ children }: PropsWithChildren<{}>) => {
if (
@@ -114,6 +116,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
+
{
+ const theme = useTheme();
+ const color = showcaseCategoryColors[item.category];
+
+ return (
+
+
+
+
+ {item.name}
+
+ {item.isRecommended && (
+
+ )}
+
+
+
+ {item.description}
+
+
+ {item.tags.slice(0, 4).map(tag => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app/src/components/showcase/ShowcasePage.tsx b/packages/app/src/components/showcase/ShowcasePage.tsx
new file mode 100644
index 0000000..4054dd3
--- /dev/null
+++ b/packages/app/src/components/showcase/ShowcasePage.tsx
@@ -0,0 +1,126 @@
+import { useMemo, useState } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import TextField from '@mui/material/TextField';
+import InputAdornment from '@mui/material/InputAdornment';
+import Chip from '@mui/material/Chip';
+import Grid from '@mui/material/Grid';
+import { Page, Header, Content } from '@backstage/core-components';
+import SearchIcon from '@mui/icons-material/Search';
+import {
+ ShowcaseCategory,
+ showcaseCategoryLabels,
+ showcaseItems,
+ filterShowcaseItems,
+ countByCategory,
+} from './showcaseConfig';
+import { ShowcaseCard } from './ShowcaseCard';
+
+const categories: ShowcaseCategory[] = [
+ 'templates',
+ 'github',
+ 'answer-engine',
+ 'platform',
+];
+
+export const ShowcasePage = () => {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState<
+ ShowcaseCategory | 'all'
+ >('all');
+
+ const counts = useMemo(() => countByCategory(showcaseItems), []);
+ const filtered = useMemo(
+ () => filterShowcaseItems(showcaseItems, selectedCategory, searchTerm),
+ [selectedCategory, searchTerm],
+ );
+
+ return (
+
+
+
+
+
+ What this portal can do
+
+
+ Start a new service from a proven template, publish it to GitHub,
+ and make it findable through the catalog and answer engine. Pick a
+ card to jump straight to the live surface.
+
+
+
+
+ setSearchTerm(e.target.value)}
+ size="small"
+ sx={{ minWidth: 300, maxWidth: 400 }}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ }}
+ />
+
+ {filtered.length} item{filtered.length !== 1 ? 's' : ''} available
+
+
+
+
+ setSelectedCategory('all')}
+ variant={selectedCategory === 'all' ? 'filled' : 'outlined'}
+ color={selectedCategory === 'all' ? 'primary' : 'default'}
+ sx={{ fontWeight: selectedCategory === 'all' ? 600 : 400 }}
+ />
+ {categories.map(category => (
+ setSelectedCategory(category)}
+ variant={selectedCategory === category ? 'filled' : 'outlined'}
+ color={selectedCategory === category ? 'primary' : 'default'}
+ sx={{
+ fontWeight: selectedCategory === category ? 600 : 400,
+ }}
+ />
+ ))}
+
+
+ {filtered.length > 0 ? (
+
+ {filtered.map(item => (
+
+
+
+ ))}
+
+ ) : (
+
+
+ No showcase items found matching "{searchTerm}"
+ {selectedCategory !== 'all' &&
+ ` in ${showcaseCategoryLabels[selectedCategory]}`}
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/app/src/components/showcase/index.ts b/packages/app/src/components/showcase/index.ts
new file mode 100644
index 0000000..2781fcb
--- /dev/null
+++ b/packages/app/src/components/showcase/index.ts
@@ -0,0 +1,11 @@
+export { ShowcasePage } from './ShowcasePage';
+export { ShowcaseCard } from './ShowcaseCard';
+export {
+ showcaseItems,
+ showcaseCategoryLabels,
+ showcaseCategoryColors,
+ filterShowcaseItems,
+ countByCategory,
+ type ShowcaseItem,
+ type ShowcaseCategory,
+} from './showcaseConfig';
diff --git a/packages/app/src/components/showcase/showcaseConfig.test.ts b/packages/app/src/components/showcase/showcaseConfig.test.ts
new file mode 100644
index 0000000..d35d96e
--- /dev/null
+++ b/packages/app/src/components/showcase/showcaseConfig.test.ts
@@ -0,0 +1,73 @@
+import {
+ showcaseItems,
+ filterShowcaseItems,
+ countByCategory,
+} from './showcaseConfig';
+
+describe('showcaseConfig', () => {
+ it('includes all seven software templates', () => {
+ const templates = showcaseItems.filter(i => i.category === 'templates');
+ expect(templates).toHaveLength(7);
+ expect(templates.map(t => t.id)).toEqual(
+ expect.arrayContaining([
+ 'template-ai-hub-service',
+ 'template-answer-engine-worker',
+ 'template-mcp-server',
+ 'template-github-blank-repo',
+ 'template-github-pull-request',
+ 'template-gitlab-blank-repo',
+ 'template-gitlab-merge-request',
+ ]),
+ );
+ });
+
+ it('includes GitHub and answer engine items', () => {
+ const counts = countByCategory(showcaseItems);
+ expect(counts.github).toBeGreaterThanOrEqual(3);
+ expect(counts['answer-engine']).toBeGreaterThanOrEqual(2);
+ expect(counts.all).toBe(showcaseItems.length);
+ });
+
+ it('every item links to a live portal surface', () => {
+ const allowed = ['/self-service', '/catalog', '/api-docs', '/search', '/docs', '/catalog-graph'];
+ showcaseItems.forEach(item => {
+ expect(allowed).toContain(item.link);
+ expect(item.linkLabel.length).toBeGreaterThan(0);
+ expect(item.description.length).toBeGreaterThan(20);
+ });
+ });
+
+ it('filters by category', () => {
+ const github = filterShowcaseItems(showcaseItems, 'github', '');
+ expect(github.every(i => i.category === 'github')).toBe(true);
+ expect(github.length).toBeGreaterThan(0);
+
+ const answer = filterShowcaseItems(showcaseItems, 'answer-engine', '');
+ expect(answer.every(i => i.category === 'answer-engine')).toBe(true);
+ });
+
+ it('filters by search term across name, description and tags', () => {
+ expect(
+ filterShowcaseItems(showcaseItems, 'all', 'mcp').map(i => i.id),
+ ).toContain('template-mcp-server');
+ expect(
+ filterShowcaseItems(showcaseItems, 'all', 'rag').length,
+ ).toBeGreaterThan(0);
+ expect(filterShowcaseItems(showcaseItems, 'all', 'zzzz-nothing')).toEqual(
+ [],
+ );
+ });
+
+ it('combines category and search', () => {
+ const result = filterShowcaseItems(showcaseItems, 'templates', 'github');
+ expect(result.length).toBeGreaterThan(0);
+ expect(result.every(i => i.category === 'templates')).toBe(true);
+ });
+
+ it('countByCategory sums to the total', () => {
+ const counts = countByCategory(showcaseItems);
+ expect(
+ counts.templates + counts.github + counts['answer-engine'] + counts.platform,
+ ).toBe(counts.all);
+ });
+});
diff --git a/packages/app/src/components/showcase/showcaseConfig.ts b/packages/app/src/components/showcase/showcaseConfig.ts
new file mode 100644
index 0000000..a5c4e69
--- /dev/null
+++ b/packages/app/src/components/showcase/showcaseConfig.ts
@@ -0,0 +1,216 @@
+export type ShowcaseCategory =
+ | 'templates'
+ | 'github'
+ | 'answer-engine'
+ | 'platform';
+
+export interface ShowcaseItem {
+ id: string;
+ name: string;
+ description: string;
+ category: ShowcaseCategory;
+ link: string;
+ linkLabel: string;
+ tags: string[];
+ isRecommended?: boolean;
+}
+
+export const showcaseCategoryLabels: Record = {
+ templates: 'Code Templates',
+ github: 'GitHub',
+ 'answer-engine': 'Answer Engine',
+ platform: 'Platform',
+};
+
+export const showcaseCategoryColors: Record = {
+ templates: '#0969da',
+ github: '#1f2328',
+ 'answer-engine': '#8250df',
+ platform: '#1a7f37',
+};
+
+export const showcaseItems: ShowcaseItem[] = [
+ {
+ id: 'template-ai-hub-service',
+ name: 'AI Hub Service',
+ description:
+ 'Scaffold a provider-agnostic gateway in front of your organisation LLM providers, with auth, rate limiting, cost tracking and audit logging.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'ai', 'llm', 'github'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-answer-engine-worker',
+ name: 'Answer Engine Worker',
+ description:
+ 'Scaffold a queue consumer that retrieves context from a document index and produces cited answers, with an indexer and query API.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'ai', 'rag', 'github'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-mcp-server',
+ name: 'MCP Server',
+ description:
+ 'Scaffold a Model Context Protocol server that AI assistants connect to for domain-specific tools and context.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'ai', 'mcp', 'github'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-github-blank-repo',
+ name: 'GitHub Blank Repo',
+ description:
+ 'Create a new blank repository in GitHub from a local skeleton and register it in the catalog.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'github', 'repo'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-github-pull-request',
+ name: 'GitHub Pull Request Docs',
+ description:
+ 'Create a documentation site in a GitHub repository and open it as a pull request for review.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'github', 'techdocs'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-gitlab-blank-repo',
+ name: 'GitLab Blank Repo',
+ description:
+ 'Create a new blank repository in GitLab from a local skeleton and register it in the catalog.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'gitlab', 'repo'],
+ isRecommended: true,
+ },
+ {
+ id: 'template-gitlab-merge-request',
+ name: 'GitLab Merge Request Docs',
+ description:
+ 'Create a documentation site in a GitLab repository and open it as a merge request for review.',
+ category: 'templates',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['recommended', 'gitlab', 'techdocs'],
+ isRecommended: true,
+ },
+ {
+ id: 'github-catalog',
+ name: 'GitHub Catalog Sync',
+ description:
+ 'Browse services, APIs and docs discovered from GitHub in the software catalog.',
+ category: 'github',
+ link: '/catalog',
+ linkLabel: 'Open catalog',
+ tags: ['github', 'catalog', 'discovery'],
+ },
+ {
+ id: 'github-scaffolder',
+ name: 'GitHub Scaffolder Actions',
+ description:
+ 'Publish new repositories and pull requests from templates with the GitHub scaffolder actions.',
+ category: 'github',
+ link: '/self-service',
+ linkLabel: 'Create component',
+ tags: ['github', 'scaffolder', 'automation'],
+ },
+ {
+ id: 'github-api-docs',
+ name: 'API Explorer',
+ description:
+ 'Explore OpenAPI definitions synced from GitHub repositories alongside the services that own them.',
+ category: 'github',
+ link: '/api-docs',
+ linkLabel: 'Explore APIs',
+ tags: ['github', 'openapi', 'api'],
+ },
+ {
+ id: 'answer-engine-search',
+ name: 'Answer Engine Search',
+ description:
+ 'Ask questions across the catalog and docs and get cited answers grounded in your own content.',
+ category: 'answer-engine',
+ link: '/search',
+ linkLabel: 'Try search',
+ tags: ['ai', 'search', 'rag'],
+ isRecommended: true,
+ },
+ {
+ id: 'answer-engine-worker-card',
+ name: 'Answer Engine Worker Template',
+ description:
+ 'Start your own answer engine worker with retrieval, citation handling and catalog registration built in.',
+ category: 'answer-engine',
+ link: '/self-service',
+ linkLabel: 'Use template',
+ tags: ['ai', 'rag', 'template'],
+ isRecommended: true,
+ },
+ {
+ id: 'platform-techdocs',
+ name: 'TechDocs',
+ description:
+ 'Read documentation that lives with the code, published from the same repository as each service.',
+ category: 'platform',
+ link: '/docs',
+ linkLabel: 'Read docs',
+ tags: ['docs', 'techdocs'],
+ },
+ {
+ id: 'platform-catalog-graph',
+ name: 'Catalog Graph',
+ description:
+ 'See how services, APIs, systems and teams relate to each other in one graph view.',
+ category: 'platform',
+ link: '/catalog-graph',
+ linkLabel: 'View graph',
+ tags: ['catalog', 'graph'],
+ },
+];
+
+export function filterShowcaseItems(
+ items: ShowcaseItem[],
+ category: ShowcaseCategory | 'all',
+ searchTerm: string,
+): ShowcaseItem[] {
+ const term = searchTerm.trim().toLowerCase();
+ return items.filter(item => {
+ const matchesCategory = category === 'all' || item.category === category;
+ if (!matchesCategory) return false;
+ if (!term) return true;
+ return (
+ item.name.toLowerCase().includes(term) ||
+ item.description.toLowerCase().includes(term) ||
+ item.tags.some(tag => tag.toLowerCase().includes(term))
+ );
+ });
+}
+
+export function countByCategory(
+ items: ShowcaseItem[],
+): Record {
+ const counts: Record = {
+ all: items.length,
+ templates: 0,
+ github: 0,
+ 'answer-engine': 0,
+ platform: 0,
+ };
+ items.forEach(item => {
+ counts[item.category] += 1;
+ });
+ return counts;
+}