Build an app page
This skill walks an empty .tsx file to a working app page that uses this repo's three-tier system correctly. The end state: a page that boots in npm run dev, renders with the right chrome and layout, has the right SEO metadata, and follows the import-tier rules from AGENTS.md.
When to use
- "Build a settings page for X"
- "Create a new dashboard for Y"
- "Add a documents page for Z"
- "Scaffold a new authenticated app screen"
- "Make a marketing landing page using our components"
- Any time the user wants a new route under
pages/that uses the design system
When NOT to use
- The user just wants to modify an existing page (use direct edits).
- The user wants a non-page React component (use the component conventions in
AGENTS.mdinstead). - The user wants App Router (
app/) page work — this repo's routes live under Pages Router (pages/). Stop and ask before introducing App Router.
Pre-flight: pin down what you're building
Before writing anything, ask yourself (or the user) four questions:
- What's the page's purpose? Marketing/landing, authenticated app screen, document viewer, dashboard, form, or gallery showcase? Each has a canonical layout pattern below.
- Does it need a session/API key? If yes, use
KeyHeaderand thegetServerSideProps→Server.setup(context)pattern (see "Authenticated app pages" below). If no, plainNavigation+getServerSidePropsreturning{ props: {} }is enough. - Does it open modals? If yes, you must render
<GlobalModalManager />and importuseModalsfrom@runtime/modals/ModalContext. - Is it a gallery showcase? If yes, you'll register it in
common/constants.tsafter writing.
Pick the page shape
Match the page to one of these canonical shapes. Each is taken straight from a working file in pages/examples/.
Shape A — Marketing / showcase page (Navigation + content + Footer)
Use when: landing pages, "show off this component" pages, content pages.
Reference: pages/examples/components/application-site.tsx.
import * as React from 'react';
import Footer from '@components/Footer';
import GlobalModalManager from '@runtime/modals/GlobalModalManager';
import Navigation from '@patterns/chrome/Navigation';
import Page from '@patterns/chrome/Page';
function ExampleMarketingPage(props) {
return (
<Page
title="wireframes.internet.dev ➝ components ➝ <something>"
description="A lightweight website template to test our design system. You can view this template on GitHub and see how we write websites."
url="https://wireframes.internet.dev/examples/components/<slug>"
>
<Navigation />
{/* page body — Demo*, sections, charts, etc. */}
<Footer />
<GlobalModalManager />
</Page>
);
}
export async function getServerSideProps(context) {
return { props: {} };
}
export default ExampleMarketingPage;
Shape B — Form page (Navigation + GridLayout + ThinAppLayout)
Use when: a single-purpose form, settings pane, sign-up flow.
Reference: pages/examples/components/forms.tsx.
import * as React from 'react';
import Button from '@components/Button';
import GlobalModalManager from '@runtime/modals/GlobalModalManager';
import GridLayout from '@elements/layouts/GridLayout';
import Input from '@elements/controls/Input';
import Navigation from '@patterns/chrome/Navigation';
import Page from '@patterns/chrome/Page';
import ThinAppLayout from '@elements/layouts/ThinAppLayout';
import { FormHeading, FormParagraph, InputLabel } from '@elements/type/forms';
function ExampleFormPage(props) {
return (
<Page
title="wireframes.internet.dev ➝ <area> ➝ <name>"
description="..."
url="https://wireframes.internet.dev/examples/<area>/<slug>"
>
<Navigation />
<GridLayout>
<ThinAppLayout style={{ background: `var(--theme-background)`, borderTop: `1px solid var(--theme-border)` }}>
<FormHeading>...</FormHeading>
<FormParagraph>...</FormParagraph>
<InputLabel style={{ marginTop: 24 }}>Field</InputLabel>
<Input style={{ marginTop: 8 }} />
<Button style={{ marginTop: 48, width: '100%' }}>Submit</Button>
</ThinAppLayout>
</GridLayout>
<GlobalModalManager />
</Page>
);
}
export async function getServerSideProps(context) {
return { props: {} };
}
export default ExampleFormPage;
Shape C — Dashboard (Navigation + DashboardWithSidebarLayout)
Use when: an app dashboard with a left sidebar of nav and a main canvas.
Reference: pages/examples/components/dashboard.tsx.
import * as React from 'react';
import DashboardWithSidebarLayout from '@elements/layouts/DashboardWithSidebarLayout';
import GlobalModalManager from '@runtime/modals/GlobalModalManager';
import Navigation from '@patterns/chrome/Navigation';
import Page from '@patterns/chrome/Page';
function ExampleDashboardPage(props) {
const sidebarElement = <div>{/* nav items */}</div>;
return (
<Page title="..." description="..." url="...">
<Navigation />
<DashboardWithSidebarLayout sidebar={sidebarElement}>
{/* main canvas */}
</DashboardWithSidebarLayout>
<GlobalModalManager />
</Page>
);
}
export async function getServerSideProps(context) {
return { props: {} };
}
export default ExampleDashboardPage;
Shape D — Authenticated app page (KeyHeader + ThinAppLayout, with session key)
Use when: the page needs the user's API key / session to function (settings, document editors, anything that talks to @common/queries).
Reference: pages/examples/empty/application-template-page.tsx.
import * as React from 'react';
import * as Server from '@common/server';
import * as Utilities from '@common/utilities';
import Cookies from '@modules/cookies';
import GlobalModalManager from '@runtime/modals/GlobalModalManager';
import KeyHeader from '@patterns/chrome/KeyHeader';
import Page from '@patterns/chrome/Page';
import ThinAppLayout from '@elements/layouts/ThinAppLayout';
import ThinAppLayoutHeader from '@elements/layouts/ThinAppLayoutHeader';
function ExampleAppPage(props) {
const [key, setKey] = React.useState<string>(props.sessionKey);
return (
<Page title="..." description="..." url="...">
<KeyHeader value={key} viewer={props.viewer} />
<ThinAppLayout>
<ThinAppLayoutHeader
token={key}
=> {
const confirm = window.confirm('Are you sure you want to sign out?');
if (!confirm) return;
setKey('');
Cookies.remove('sitekey');
window.location.reload();
}}
/>
{/* page body */}
</ThinAppLayout>
<GlobalModalManager />
</Page>
);
}
export async function getServerSideProps(context) {
const { sessionKey, viewer } = await Server.setup(context);
return { props: { sessionKey, viewer } };
}
export default ExampleAppPage;
Shape E — Three-column document workspace (sidebar + details list + editor canvas)
Use when: an app where a sidebar drives a list, the list drives a detail view (e.g. statement-of-work editor, invoice editor).
Reference: pages/examples/features/statement-of-work/index.tsx.
import KeyHeader from '@patterns/chrome/KeyHeader';
import ThreeColumnAppLayout from '@elements/layouts/ThreeColumnAppLayout';
// ...
<ThreeColumnAppLayout sidebar={sidebar} details={details}>
{/* main editor */}
</ThreeColumnAppLayout>
Step-by-step procedure
Step 1 — pick the file path
Routes live under pages/. Match the existing groupings:
pages/examples/components/<slug>.tsx— UI showcase (marketing, components, layouts)pages/examples/empty/<slug>.tsx— blank starter templatespages/examples/features/<slug>.tsx— feature demos with backend (auth, files, documents)pages/examples/system/<slug>.tsx— design-system reference (typography, colors)pages/examples/animations/<slug>.tsx— motion/animation demospages/examples/fonts/<slug>.tsx— font specimens
For dynamic routes use [id].tsx. For new top-level areas, ask the user before creating.
Step 2 — drop in the right shape
Copy the matching shape from above (A–E). Replace the placeholder title/description/url with real values. The url should be the absolute URL on wireframes.internet.dev matching the file path.
Step 3 — apply the import-tier rules
From AGENTS.md:
- Import atoms from
@elements/<bucket>/Foo(icons, type, controls, marks, layouts, sections, scroll, charts, visuals, motion, diagrams, shells) - Import molecules from
@components/Foo(Button, Checkbox, Select, Table, Footer, FormUpload, MonospacePreview, BlockFade, FadeManager, CheckmarkItem, FormChangePassword, FormSettingsPrivacy) - Import patterns from
@patterns/<bucket>/Foo(chrome, modals, demos) - Import runtime from
@runtime/<bucket>/Foo(modals, detectors, testing) - Never use
../../across top-level dirs. Use./Fooonly for siblings in the same folder. - Group imports: styles → React → utilities (
@common,@root) → in-repo components (lowest tier first) → typography from@elements/type.
Step 4 — choose layouts deliberately
Layouts live in @elements/layouts/:
| Layout | Width | Use for |
|---|---|---|
AppLayout |
768px | Standard app body with side borders |
ThinAppLayout |
512px | Forms, document editors, narrow flows |
WideAppLayout |
wider than 768 | Marketing canvas with breathing room |
GridLayout |
full width | Wireframe-grid background; usually wraps another layout |
Content |
constrained | Documentation/text content |
TwoColumnLayout / TwoColumnLayoutFull / TwoColumnLayoutSidebar |
full width | Asymmetric two-column splits |
ThreeColumnAppLayout |
full width | Sidebar + details + canvas (Shape E) |
DashboardWithSidebarLayout |
full width | Dashboards (Shape C) |
IsometricGridLayout |
full width | Decorative isometric grid |
CardHandLayout |
full width | Fanned-card display |
InvoiceLayout |
document-shaped | Invoice/SOW print layout |
All full-height layouts use min-height: calc(100dvh - 48px) (the 48 accounts for the Navigation row). If your page has no <Navigation />, override with style={{ minHeight: '100dvh' }} on the layout.
Step 5 — sections (for vertical rhythm)
If your page has multiple stacked "screens" of content (landing-page hero, features, CTA, etc.), wrap each in:
@elements/sections/SectionFullHeight— fills the viewport@elements/sections/SectionHalfHeight— half-viewport@elements/sections/SectionHorizontalStack— horizontal panels
Step 6 — wire modals (only if needed)
If the page opens any modal (auth flow, error popup, color picker, navigation menu, etc.):
import { useModals } from '@runtime/modals/ModalContext';
import ModalError from '@patterns/modals/ModalError';
const modals = useModals();
modals.open(ModalError, { message: '...' });
You must render <GlobalModalManager /> somewhere in the page tree (typically just before </Page>). Without it, modals open in state but never render. Already included in shapes A–E above.
The modal context is provided globally by <Providers> in pages/_app.tsx — you don't wrap the page yourself.
Step 7 — typography
- Marketing/large copy:
H1, H2, H3, H4, H5, Lead, SubLead, Pfrom@elements/type(modularremscale, shrinks on mobile). - Form labels and form copy:
FormHeading, FormSubHeading, FormParagraph, InputLabelfrom@elements/type/forms. - UI chrome (table cells, button labels, tooltips):
Title, Text, SubTitle, SubText, UnitLabelfrom@elements/type(fixedpxscale, doesn't shrink).
Don't use raw <h1>/<p> tags — they get the global reset (zero margin, no scale).
Step 8 — spacing
Use inline style={{ marginTop: 16 }} for one-off spacing. Stick to multiples of 4 (4, 8, 12, 16, 24, 32, 48, 64). Don't add new CSS rules in the page file — pages are composition, not styling.
Step 9 — getServerSideProps
Every page in this repo exports getServerSideProps, even when no data is fetched. The empty form is { props: {} }. Match the convention.
For authenticated pages, use:
export async function getServerSideProps(context) {
const { sessionKey, viewer } = await Server.setup(context);
return { props: { sessionKey, viewer } };
}
Step 10 — register in common/constants.ts (if it's a gallery page)
If the page belongs in the example gallery on /examples, add an entry to the matching TEMPLATE_EXAMPLES_* array in common/constants.ts:
TEMPLATE_EXAMPLES_ANIMATIONS— forpages/examples/animations/*TEMPLATE_EXAMPLES_COMPONENTS— forpages/examples/components/*TEMPLATE_EXAMPLES_EMPTY— forpages/examples/empty/*TEMPLATE_EXAMPLES_FEATURES— forpages/examples/features/*TEMPLATE_EXAMPLES_SYSTEM— forpages/examples/system/*
Entry shape:
{
name: 'Components ➝ navigation, <thing>',
href: '/examples/components/<slug>',
label: '<short label>',
// optional: isWIP: true, internal: true
}
The name uses ➝ (Unicode arrow) as a category separator — match existing entries exactly. internal: true hides it from public view; isWIP: true flags it as work-in-progress.
Step 11 — verify
- Boot
npm run dev. The dev server runs on port 10000. - Open
http://localhost:10000/examples/<area>/<slug>. - Check: page renders, theme switching works (click "Theme" in
Navigation), no console errors, layout doesn't shift on hover/focus. - Run
npm run buildbefore declaring done — Next 16 with Turbopack typechecks the build, which catches missing imports and bad prop types that the dev server may tolerate.
Anti-patterns
- Don't import a page from another page. Pages are leaves of the dependency tree. If you find yourself wanting to share UI across pages, factor it into
@patterns/<bucket>/Fooinstead. - Don't add new CSS rules in the page file. Pages compose, components style. If a layout doesn't fit, choose a different layout — don't override its CSS via
style={{}}beyond simple spacing/colors. - Don't skip
<GlobalModalManager />"because this page doesn't open modals yet." Including it is free and prevents a regression when a modal gets added later. Every shape above includes it. - Don't add a
<head>block manually.<Page>already wires<Head>with the right meta tags, OG/Twitter cards, and favicons. Pass title/description/url and you're done. - Don't introduce a new top-level alias. Use
@elements,@components,@patterns,@runtime,@common,@modules,@root,@pages— that's it. - Don't add a Props interface unless the file you're matching has one. The codebase convention is loose props (
function Foo(props) { ... }). - Don't introduce a UI library, Tailwind, or styled-components. This is a vanilla-CSS repo. See
AGENTS.md§ "What NOT to do". - Don't write tests for the page. This repo has no test runner; testing is visual via
npm run dev.
Quick reference: minimum viable page
If the user just wants the smallest possible working page, this is it (Shape A, no Footer):
import * as React from 'react';
import GlobalModalManager from '@runtime/modals/GlobalModalManager';
import Navigation from '@patterns/chrome/Navigation';
import Page from '@patterns/chrome/Page';
function ExampleMyNewPage(props) {
return (
<Page
title="wireframes.internet.dev ➝ <area> ➝ <name>"
description="A lightweight website template to test our design system."
url="https://wireframes.internet.dev/examples/<area>/<slug>"
>
<Navigation />
{/* your content */}
<GlobalModalManager />
</Page>
);
}
export async function getServerSideProps(context) {
return { props: {} };
}
export default ExampleMyNewPage;
Then add the entry to common/constants.ts and verify in browser.