From automation
Guide to building Next.js page shells — metadata, params, header pattern selection, content wrappers, and structural concerns. Pages are thin shells; content is handled by other skills.
npx claudepluginhub joshuarweaver/cascade-code-languages-misc-1 --plugin finstreet-fe-claude-pluginsThis skill uses the workspace's default tool permissions.
Pages are thin shells providing metadata, a header, and a content wrapper. They do NOT own what goes inside the wrapper — forms, lists, task groups, and inquiry content are handled by other skills.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Pages are thin shells providing metadata, a header, and a content wrapper. They do NOT own what goes inside the wrapper — forms, lists, task groups, and inquiry content are handled by other skills.
Every page exports a static metadata object. Title is always a plain German string — never use translations.
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
export const metadata: Metadata = {
title: `Seitentitel | ${Constants.companyName}`,
};
Derive from the file path — every [segment] becomes a param. Always Promise, always await before destructuring.
// Path: src/app/operations/weg-konten/[financingCaseId]/verrechnungskonto/page.tsx
type Props = {
params: Promise<{ financingCaseId: string }>;
};
export default async function SomePage({ params }: Props) {
const { financingCaseId } = await params;
// ...
}
Multiple params accumulate from all segments in the path:
// Path: src/app/operations/anfragen/[inquiryId]/[stepId]/page.tsx
type Props = {
params: Promise<{ inquiryId: string; stepId: string }>;
};
List pages — use nuqs SearchParams type with a search params cache:
import { SearchParams } from "nuqs/server";
type Props = {
searchParams: Promise<SearchParams>;
};
export default async function ListPage({ searchParams }: Props) {
const resolvedSearchParams = await searchParams;
const { search, pagination } = someSearchParamsCache.parse(resolvedSearchParams);
// ...
}
Auth pages — use explicit typed searchParams:
type Props = {
searchParams: Promise<{
passwordResetSuccess: string;
redirectTo: string;
}>;
};
export default async function AuthPage(props: Props) {
const searchParams = await props.searchParams;
// ...
}
Choose the correct header based on page context:
Inquiry step page? (inside an inquiry process) → No header in page.tsx. The page is a thin wrapper that delegates to a feature page component. The feature component renders InquiryHeader + InquiryContent. See the inquiry-process step-page guide.
Auth page? (login, password reset, etc.) → No header. Use Panel + VStack as container.
Sub-page under a detail page? (e.g., [id]/verrechnungskonto/page.tsx) → PageHeader with PageHeaderBackButton + PageHeaderTitle. Often uses a shared header component like FspFinancingCaseOverviewSubPageHeader.
List page or standard portal page? → PageHeader with PageHeaderTitle + optional PageHeaderActions.
Detail overview page? (e.g., [id]/page.tsx showing status/tasks) → Custom header component extracted to a feature, built on PageHeader primitives.
For reusable header components, when to use them vs composing inline, and where to store new ones, see headers.md.
PageContent — standard portal pages (list, overview, sub-page form). Import from @finstreet/ui/components/pageLayout/PageContent.InquiryContent — inquiry process steps. Used inside the feature page component, not in page.tsx. Import from @finstreet/ui/components/pageLayout/InquiryContent.Panel — auth pages. No PageContent needed. Import from @finstreet/ui/components/base/Panel.Content inside these wrappers is provided by other skills.
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
import {
PageHeader,
PageHeaderActions,
PageHeaderTitle,
} from "@finstreet/ui/components/pageLayout/PageHeader";
import { Headline } from "@finstreet/ui/components/base/Headline";
import { PageContent } from "@finstreet/ui/components/pageLayout/PageContent";
import { getExtracted } from "next-intl/server";
export const metadata: Metadata = {
title: `Seitentitel | ${Constants.companyName}`,
};
export default async function {PageName}Page() {
const t = await getExtracted();
return (
<>
<PageHeader>
<PageHeaderTitle>
<Headline as="h1">{t("{German title}")}</Headline>
</PageHeaderTitle>
<PageHeaderActions>{/* Action buttons */}</PageHeaderActions>
</PageHeader>
<PageContent>
{/* Content from other skills */}
</PageContent>
{/* Modals at the bottom */}
</>
);
}
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
import { PageContent } from "@finstreet/ui/components/pageLayout/PageContent";
import { SearchParams } from "nuqs/server";
import { Suspense } from "react";
import { ListSkeleton } from "@finstreet/ui/components/base/Skeletons/ListSkeleton";
export const metadata: Metadata = {
title: `Seitentitel | ${Constants.companyName}`,
};
export const dynamic = "force-dynamic";
type Props = {
searchParams: Promise<SearchParams>;
};
export default async function {PageName}Page({ searchParams }: Props) {
const resolvedSearchParams = await searchParams;
const { search, pagination } = {feature}SearchParamsCache.parse(resolvedSearchParams);
return (
<>
{/* PageHeader with title + optional actions */}
<PageContent>
<Suspense fallback={<ListSkeleton />}>
{/* List component */}
</Suspense>
</PageContent>
{/* Modals at the bottom */}
</>
);
}
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
import { FspFinancingCaseOverviewSubPageHeader } from "@/layouts/fsp/FspFinancingCaseOverviewSubPageHeader";
import { PageContent } from "@finstreet/ui/components/pageLayout/PageContent";
export const metadata: Metadata = {
title: `Seitentitel | ${Constants.companyName}`,
};
type Props = {
params: Promise<{ financingCaseId: string }>;
};
export default async function {PageName}Page({ params }: Props) {
const { financingCaseId } = await params;
return (
<>
<FspFinancingCaseOverviewSubPageHeader
title={t("formTitle")}
financingCaseId={financingCaseId}
header={response.header}
/>
<PageContent>
{/* Content from other skills */}
</PageContent>
</>
);
}
When no shared header component exists, compose directly with PageHeader primitives:
<PageHeader>
<PageHeaderBackButton href={backUrl}>{t("back")}</PageHeaderBackButton>
<PageHeaderTitle>
<Headline as="h1">{title}</Headline>
</PageHeaderTitle>
</PageHeader>
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
import { {StepName}Page } from "@/features/{purpose}InquiryProcess/components/{StepName}Page";
export const metadata: Metadata = {
title: `{Step Title} | ${Constants.companyName}`,
};
type Props = {
params: Promise<{ inquiryId: string }>;
};
export default async function FSP{StepName}Page({ params }: Props) {
const { inquiryId } = await params;
return <{StepName}Page inquiryId={inquiryId} />;
}
Header and content wrapper live in the feature page component. See inquiry-process step-page guide.
import { Metadata } from "next";
import { Constants } from "@/shared/utils/constants";
import { Panel } from "@finstreet/ui/components/base/Panel";
import { Headline } from "@finstreet/ui/components/base/Headline";
import { VStack } from "@styled-system/jsx";
import { getExtracted } from "next-intl/server";
export const metadata: Metadata = {
title: `Anmelden | ${Constants.companyName}`,
};
export default async function {PageName}Page() {
const t = await getExtracted();
return (
<Panel p={8}>
<VStack gap={8} alignItems="stretch">
<Headline as="h1">{t("headline")}</Headline>
{/* Banners for status messages */}
{/* Auth form component */}
</VStack>
</Panel>
);
}
PageContent.export const dynamic = "force-dynamic": only on list pages that need fresh data on every request.ResetScrollPosition: use on detail overview pages at the top of the JSX, before the header. Import from @finstreet/ui/components/base/ResetScrollPosition.<Suspense fallback={<ListSkeleton />}> inside PageContent.async — pages are Server Componentsawait params / await searchParams before destructuringgetExtracted() (server) or useExtracted() (client)PageContentHello World as placeholderPageHeaderActions is omitted entirely when there are no actions — do not render an empty one