diff --git a/.tmp/proof/landing-hero.png b/.tmp/proof/landing-hero.png
new file mode 100644
index 0000000..f753509
Binary files /dev/null and b/.tmp/proof/landing-hero.png differ
diff --git a/packages/app/public/portal-showcase.png b/packages/app/public/portal-showcase.png
new file mode 100644
index 0000000..317ea3a
Binary files /dev/null and b/packages/app/public/portal-showcase.png differ
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index ae1f34c..fe40ca1 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -54,6 +54,7 @@ import { ScorecardPage } from './components/scorecard';
import { PulseCheckPage } from './components/pulse-check';
import { ExplorePage } from './components/explore';
import { ShowcasePage } from './components/showcase';
+import { LandingPage } from './components/landing';
const app = createApp({
apis,
@@ -202,6 +203,7 @@ const routes = (
} />
} />
} />
+ } />
);
diff --git a/packages/app/src/components/landing/LandingPage.test.tsx b/packages/app/src/components/landing/LandingPage.test.tsx
new file mode 100644
index 0000000..6381afe
--- /dev/null
+++ b/packages/app/src/components/landing/LandingPage.test.tsx
@@ -0,0 +1,46 @@
+import { screen } from '@testing-library/react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { LandingPage } from './LandingPage';
+
+describe('LandingPage', () => {
+ it('renders the hero headline and value prop', async () => {
+ await renderInTestApp();
+
+ expect(
+ screen.getByText('Everything your developers need, in one portal'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Software templates that publish to GitHub/),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Try it as a guest, no account needed/),
+ ).toBeInTheDocument();
+ });
+
+ it('links the primary call to action into the showcase', async () => {
+ await renderInTestApp();
+
+ const cta = screen.getByRole('link', { name: 'Explore the showcase' });
+ expect(cta).toHaveAttribute('href', '/showcase');
+ });
+
+ it('links the secondary call to action into the catalog', async () => {
+ await renderInTestApp();
+
+ const cta = screen.getByRole('link', { name: 'Browse the catalog' });
+ expect(cta).toHaveAttribute('href', '/catalog');
+ });
+
+ it('shows the portal screenshot with alt text', async () => {
+ await renderInTestApp();
+
+ expect(
+ screen.getByAltText(
+ 'The plugin showcase in this portal, listing templates, GitHub integration and the answer engine',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('The plugin showcase running live in this portal.'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/packages/app/src/components/landing/LandingPage.tsx b/packages/app/src/components/landing/LandingPage.tsx
new file mode 100644
index 0000000..ddaba5a
--- /dev/null
+++ b/packages/app/src/components/landing/LandingPage.tsx
@@ -0,0 +1,113 @@
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import { styled, useTheme } from '@mui/material/styles';
+import { Link as RouterLink } from 'react-router-dom';
+import { Page, Content } from '@backstage/core-components';
+
+const HeroSection = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(3),
+ padding: theme.spacing(8, 4),
+ borderRadius: theme.spacing(1),
+ border: `3px solid ${theme.palette.text.primary}`,
+ boxShadow: `10px 10px 0 ${theme.palette.text.primary}`,
+ backgroundColor: theme.palette.background.paper,
+ [theme.breakpoints.down('md')]: {
+ padding: theme.spacing(5, 2),
+ boxShadow: `6px 6px 0 ${theme.palette.text.primary}`,
+ },
+}));
+
+const ScreenshotFrame = styled('figure')(({ theme }) => ({
+ margin: 0,
+ padding: theme.spacing(1.5),
+ borderRadius: theme.spacing(1),
+ border: `3px solid ${theme.palette.text.primary}`,
+ boxShadow: `10px 10px 0 ${theme.palette.text.primary}`,
+ backgroundColor: theme.palette.background.paper,
+ [theme.breakpoints.down('md')]: {
+ boxShadow: `6px 6px 0 ${theme.palette.text.primary}`,
+ },
+}));
+
+export const LandingPage = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+ Everything your developers need, in one portal
+
+
+ Software templates that publish to GitHub, a catalog that keeps
+ services findable, and an answer engine grounded in your own docs.
+ Try it as a guest, no account needed.
+
+
+
+
+
+
+
+
+
+
+ The plugin showcase running live in this portal.
+
+
+
+
+ );
+};
diff --git a/packages/app/src/components/landing/index.ts b/packages/app/src/components/landing/index.ts
new file mode 100644
index 0000000..a91a96a
--- /dev/null
+++ b/packages/app/src/components/landing/index.ts
@@ -0,0 +1 @@
+export { LandingPage } from './LandingPage';