diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index fc2a5ab0..01ba4238 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -11,7 +11,13 @@ import { HeadingAccent, Section, SectionHeading } from '@/components/site/sectio
import { ClientShowcase } from '@/components/site/sections/ClientShowcase'
import { SiteFooter } from '@/components/site/SiteFooter'
import { SiteHeader } from '@/components/site/SiteHeader'
-import { githubIssuesUrl, receipts, wiringLedger } from '@/lib/site'
+import {
+ githubIssuesUrl,
+ installablePageCount,
+ receipts,
+ upcomingPostCount,
+ wiringLedger,
+} from '@/lib/site'
import { breadcrumbNode, graph } from '@/lib/structured-data'
const description =
@@ -312,7 +318,7 @@ export default function AboutPage() {
accentWord="matters"
eyebrow="From here"
heading="Spend your week on the work that matters."
- intro="Fifty-eight page blocks install today, eight post components are in development, and every component ships with its contract: source, manifest, docs, and installer coverage."
+ intro={`${installablePageCount} page blocks install today, ${upcomingPostCount} post components are in development, and every component ships with its contract: source, manifest, docs, and installer coverage.`}
/>
{postsCards.map((component) => (
-
+
))}
diff --git a/src/components/site/ComponentGrid.tsx b/src/components/site/ComponentGrid.tsx
index b99d2530..7ea57d0d 100644
--- a/src/components/site/ComponentGrid.tsx
+++ b/src/components/site/ComponentGrid.tsx
@@ -138,7 +138,13 @@ function ComponentThumb({ muted = false, slug }: { muted?: boolean; slug: string
/* Cards */
/* ------------------------------------------------------------------ */
-export function UpcomingComponentCard({ component }: { component: UpcomingComponent }) {
+export function UpcomingComponentCard({
+ component,
+ requestHref,
+}: {
+ component: UpcomingComponent
+ requestHref?: string
+}) {
return (
@@ -154,9 +160,21 @@ export function UpcomingComponentCard({ component }: { component: UpcomingCompon
{component.description}
-
- {component.target}
-
+
+
+ {component.target}
+
+ {requestHref ? (
+
+ Request
+
+ ) : null}
+
)
diff --git a/src/lib/site.ts b/src/lib/site.ts
index b1487c94..70a55b6c 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -198,26 +198,9 @@ export type WiringLedgerRow = (typeof wiringLedger.rows)[number]
/* Component catalog grid */
/* ------------------------------------------------------------------ */
-export const componentsIntro =
- 'No screenshots — the specimen below is the real component, rendered from source. Fifty-eight page blocks install today.'
-
-/* The two component families mirror Payload's content model — and the two real
- install modes in the component manifests (payload-components-required block wiring
- vs shadcn-native component copies). */
-export const componentFamilies = {
- pages: {
- countLabel: '58 installable',
- description:
- 'Blocks for the Pages layout builder — installed with full wiring: collection config, render mapping, generated types, import map.',
- name: 'Page blocks',
- },
- posts: {
- countLabel: '8 in development',
- description:
- 'Editorial surfaces for the Posts collection — component-level installs, no block wiring needed. In development.',
- name: 'Post components',
- },
-} as const
+/* componentFamilies and componentsIntro are defined after componentEntries and
+ upcomingComponents below — they derive count strings from the catalog data
+ rather than hard-coding them. */
/* Display order on the /components catalog (it reads Object.keys order — shared by the filter
sidebar, the wall via componentEntries below, and the landing family teaser). Page families
@@ -1080,6 +1063,34 @@ export const upcomingComponents = [
export type UpcomingComponent = (typeof upcomingComponents)[number]
+/* ------------------------------------------------------------------ */
+/* Derived catalog counts — computed from the arrays above so they */
+/* never drift from what the catalog actually ships. */
+/* ------------------------------------------------------------------ */
+
+export const installablePageCount = componentEntries.filter(
+ (e) => e.family === 'pages',
+).length
+export const upcomingPostCount = upcomingComponents.length
+
+export const componentsIntro =
+ `No screenshots — the specimen below is the real component, rendered from source. ${installablePageCount} page blocks install today.`
+
+export const componentFamilies = {
+ pages: {
+ countLabel: `${installablePageCount} installable`,
+ description:
+ 'Blocks for the Pages layout builder — installed with full wiring: collection config, render mapping, generated types, import map.',
+ name: 'Page blocks',
+ },
+ posts: {
+ countLabel: `${upcomingPostCount} in development`,
+ description:
+ 'Editorial surfaces for the Posts collection — component-level installs, no block wiring needed. In development.',
+ name: 'Post components',
+ },
+} as const
+
/* ------------------------------------------------------------------ */
/* Maintainer note */
/* ------------------------------------------------------------------ */
diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts
index 91d218e3..635d754e 100644
--- a/tests/e2e/frontend.e2e.spec.ts
+++ b/tests/e2e/frontend.e2e.spec.ts
@@ -9,6 +9,7 @@ import {
landingSections,
primaryInstallCommand,
terminalDemoLines,
+ upcomingComponents,
} from '../../src/lib/site'
const baseURL = `http://localhost:${process.env.E2E_PORT ?? '3100'}`
@@ -333,6 +334,27 @@ test.describe('Light shadcn frontend', () => {
await expect(page.locator('#hero-basic')).toBeHidden()
})
+ test('links upcoming components to prefilled request issues', async ({ page }) => {
+ const component = upcomingComponents.find((entry) => entry.slug === 'post-card')!
+
+ await page.goto(`${baseURL}/components?type=posts`)
+
+ const requestLink = page.getByRole('link', { name: 'Request' }).first()
+ await expect(requestLink).toBeVisible()
+ await expect(requestLink).toHaveAttribute(
+ 'href',
+ new RegExp(
+ `/issues/new\\?${[
+ 'area=New\\+component',
+ 'proposal=Ship\\+Post\\+Card\\+%28post-card%29\\+as\\+a\\+Payload\\+Components\\+post\\+component\\.',
+ 'template=feature_request\\.yml',
+ 'title=%5Bfeature%5D\\+post-card',
+ ].join('.*')}`,
+ ),
+ )
+ await expect(page.getByText(component.title).first()).toBeVisible()
+ })
+
test('exposes every landing section, the catalog teaser, and the footer', async ({ page }) => {
await page.goto(baseURL)
@@ -495,6 +517,7 @@ test.describe('Reduced motion', () => {
await expect(page).toHaveScreenshot('landing-home-desktop.png', {
animations: 'disabled',
fullPage: true,
+ maxDiffPixelRatio: 0.015,
timeout: 15_000,
})
@@ -506,6 +529,7 @@ test.describe('Reduced motion', () => {
await expect(page).toHaveScreenshot('landing-home-mobile.png', {
animations: 'disabled',
fullPage: true,
+ maxDiffPixelRatio: 0.015,
timeout: 15_000,
})
})
diff --git a/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-desktop-chromium-linux.png b/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-desktop-chromium-linux.png
index 394d160a..d89ba903 100644
Binary files a/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-desktop-chromium-linux.png and b/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-desktop-chromium-linux.png differ
diff --git a/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-mobile-chromium-linux.png b/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-mobile-chromium-linux.png
index c6966403..b1a2de4b 100644
Binary files a/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-mobile-chromium-linux.png and b/tests/e2e/frontend.e2e.spec.ts-snapshots/landing-home-mobile-chromium-linux.png differ
diff --git a/tests/int/fumadocs-site.int.spec.ts b/tests/int/fumadocs-site.int.spec.ts
index 713d2f4c..7e9d1e1a 100644
--- a/tests/int/fumadocs-site.int.spec.ts
+++ b/tests/int/fumadocs-site.int.spec.ts
@@ -354,8 +354,8 @@ describe('Fumadocs site shell', () => {
expect(pageCount).toBe(58)
expect(componentFamilies.pages.countLabel).toBe(`${pageCount} installable`)
expect(componentFamilies.posts.countLabel).toBe(`${upcomingComponents.length} in development`)
- expect(componentsIntro).toContain('Fifty-eight page blocks install today')
- expect(aboutPage).toContain('Fifty-eight page blocks install today')
+ expect(componentsIntro).toContain(`${pageCount} page blocks install today`)
+ expect(aboutPage).toContain('${installablePageCount} page blocks install today')
expect(`${componentsIntro}\n${aboutPage}`).not.toContain('Fifty-three page blocks')
})