Add a new page
This skill scaffolds the page layout from a per-type template and wires the page into every system that must know about it (navigation, metadata, server guard, route types, analytics, sitemap).
Two phases. The scaffold phase (Steps 0–9) produces layout skeletons with clearly-marked TODOs. The
optional data-wiring phase (Step 10, procedure in wiring.md) then connects those skeletons to real API
resources and renders the data plainly — Step 0 decides whether it runs now or is deferred. Filtering,
sorting, search, socket live-updates, mocks, tests, and design polish are out of scope in both phases. If
wiring is deferred, the freshly-scaffolded page keeps its intentional TODOs and will not fully pass
lint/tsc until its data is wired — that is expected.
TODO tags. Every deferred piece of work is marked with a stage-tagged TODO, so each later stage can find
its worklist with a grep: TODO (api-data): — resolved by the data-wiring phase (wiring.md);
TODO (design): — content/representation decisions left for the design stage (icons, field hints,
uncertain value formatting). Keep the tags when scaffolding, and use them for any new deferred work you mark.
Rule of thumb: never guess. Whenever information is missing or required to proceed, pause and ask the user, or confirm your findings, before writing files.
Templates
Templates live in .agents/skills/add-new-page/templates/. Copy the set matching the page type, then
substitute the placeholders and delete any optional blocks you don't need:
| Placeholder | Meaning | Example |
|---|---|---|
__PageName__ |
PascalCase component name | Validators, ValidatorDetails |
__area__ |
features (config-gated) or slices (core) |
features |
__featureName__ |
folder name + config.features key |
validators |
__pageDir__ |
index (root page) or details (detail page) |
index |
__route__ |
the route path | /validators |
__pathname__ |
the typed pathname (route, or with [param]) |
/validators/[id] |
__title__ |
<PageTitle> text |
Validators |
__entityParam__ |
dynamic-route param name (detail pages) | id, hash |
__gsspName__ |
getServerSideProps export name in main.ts |
validators |
Template sets:
templates/general/{Page,GeneralInfo}.tsx.tmpl(Page= non-tabbed general page;GeneralInfo= a general-content body, used as a tab panel)templates/detail/{Page,Details}.tsx.tmpltemplates/index/{Page,Content,Table,TableItem,List,ListItem}.tsx.tmpltemplates/tabs/PageWithTabs.tsx.tmpl(tabbed page shell —PageTitle+RoutedTabs)templates/route.tsx.tmpl(shared route wrapper; has a commented dynamic-route variant)
A page shell is one of: a non-tabbed Page.tsx.tmpl (embeds a single content body) or the tabbed
PageWithTabs.tsx.tmpl (renders RoutedTabs). A content body is Details (detail), Content +
Table/List/items (index), or GeneralInfo (general). Tabbed pages combine one shell with one body per tab.
Naming the destination files. Drop the .tmpl extension and name each component file after the
component it exports — i.e. the __PageName__-prefixed name used in the template's const / import
statements. The base Page.tsx.tmpl is special: it becomes just __PageName__.tsx (no "Page" suffix). The
route wrapper is named after the route, not the component.
| Template | Destination (for __PageName__ = Validators) |
|---|---|
general/Page.tsx.tmpl |
__PageName__.tsx → Validators.tsx |
detail/Page.tsx.tmpl |
__PageName__.tsx → Validators.tsx |
detail/Details.tsx.tmpl |
__PageName__Details.tsx → ValidatorsDetails.tsx |
index/Page.tsx.tmpl |
__PageName__.tsx → Validators.tsx |
index/Content.tsx.tmpl |
__PageName__Content.tsx → ValidatorsContent.tsx |
index/Table.tsx.tmpl |
__PageName__Table.tsx → ValidatorsTable.tsx |
index/TableItem.tsx.tmpl |
__PageName__TableItem.tsx → ValidatorsTableItem.tsx |
index/List.tsx.tmpl |
__PageName__List.tsx → ValidatorsList.tsx |
index/ListItem.tsx.tmpl |
__PageName__ListItem.tsx → ValidatorsListItem.tsx |
general/GeneralInfo.tsx.tmpl |
__PageName__GeneralInfo.tsx → ValidatorsGeneralInfo.tsx |
tabs/PageWithTabs.tsx.tmpl |
__PageName__.tsx → Validators.tsx |
route.tsx.tmpl |
src/pages/__route__.tsx → src/pages/validators.tsx |
Tabbed pages — folders & per-tab names. The page shell sits directly in the page dir; each tab's body
components live in their own kebab-case sub-folder under it (mirrors
src/slices/tx/pages/details/{info,logs,state,…}). Name a tab's components after the tab so same-type tabs
don't collide: replace the __PageName__ base with __PageName__<TabName> (PascalCase tab name), and rename
the body's sub-components/imports to match. E.g. an index-type tab "Active" on page Validators → folder
active/ holding ValidatorsActive (from Content), ValidatorsActiveTable, ValidatorsActiveTableItem,
ValidatorsActiveList, ValidatorsActiveListItem. A detail-type tab "Details" → folder info/ holding
ValidatorsDetails (from Details). A general-type tab "Stats" → folder stats/ holding ValidatorsStats
(from GeneralInfo).
Details-tab rule: a details-type tab either comes first (id 'index', title Details) or doesn't
exist at all — never place it after another tab.
Step 0 — Agree on the shape (REQUIRED before writing anything)
Propose each of the following and wait for the user's explicit approval. Ask whenever an answer is not obvious from the conversation or the codebase.
- Layout — does the page have tabs? Decide this first; ask if it isn't obvious from the conversation.
- No tabs → pick the single content type:
index(list + table views),detail(label/value grid), orgeneral(title + content). - Tabs → list the tabs in order; for each, give a name (the tab title) and a content type
(
details/index/general info). That's enough at this stage. Adetailstab, if present, must be the first tab.
- No tabs → pick the single content type:
- Route & path params — the exact path. If it's dynamic (
/foo/[param]), confirm the param name(s) and what each represents (__entityParam__,__pathname__). - Gated or core? — apply the architecture rule's test: "Can this exist on a vanilla EVM chain with no
feature flag?" If yes → core slice (
__area__=slices, no feature config/sitemap). If no → config-gated feature (__area__=features). - Title and navigation placement — the
<PageTitle>text, the nav label, and which nav group it belongs to. - Metadata — suggest a default page title and description; let the user confirm or refine. Default
only — do not add
enhancedvariants. - Data — wire data now, or layout only? If now, collect for each content body (per tab if tabbed):
the API service + endpoint path that feeds it — given by the user, never hunted for (the
service:namekey if the resource is already declared) — and the live instance to sample data from (atools/dev-server/registry.jsonalias or a full URL; a new endpoint may exist only on staging). If the endpoint isn't deployed anywhere yet, wiring is deferred — layout only. For each resource that doesn't exist yet, also run the Step 0 interview of theadd-api-resourceskill here — after this step, no question should remain for the middle of the work.
Proceed only once these are settled.
Step 1 — Feature & env variable (config-gated pages only)
Skip this step entirely for core slice pages.
The route's server guard (Step 3) depends on the feature flag, so the feature and its env variable must exist before the route file. If the feature and its config already exist, proceed to the next step.
- Invoke the
add-env-varskill to create the guarding env variable(s), the featuresrc/features/__featureName__/config.ts, and its registration insrc/config/features.ts. That skill interviews the user about the new feature and its variable(s) — all env-var questions happen there, not here. add-env-varcoversdocs/ENVS.md,config.ts,features.ts, the validator schema, and CSP/assets. It does not create the server guard — that lives in Step 3.
Step 2 — Scaffold the page component(s) (layout only)
Create the component folder, mirroring src/slices/block/pages/{index,details}. __pageDir__ is details
for an entity/detail page (typically a dynamic route), otherwise index:
src/__area__/__featureName__/pages/__pageDir__/
The content bodies are the same regardless of tabs — reference these exemplars while filling in real layout:
- general (
GeneralInfo.tsx, or inline in a non-tabbedgeneral/Page.tsx) — a content slot. Exemplarsrc/features/gas-tracker/pages/index/GasTracker.tsx. - detail (
Details.tsx) — aDetailedInfo.Containergrid ofItemLabel/ItemValuerows wrapped inSkeleton. Exemplarssrc/slices/block/pages/details/BlockDetails.tsx; gridsrc/shared/detailed-info/DetailedInfo.tsx. - index (
Content.tsx+Table/TableItem+List/ListItem) —Content.tsxisDataList+ dual view (<Box hideFrom="lg">mobileList,<Box hideBelow="lg">desktopTable);Table/Listrender one row per item via dedicatedTableItem/ListItemcomponents (mirroringBlocksTableItem/BlocksListItem). Exemplarssrc/slices/block/pages/index/{BlocksContent,BlocksTable,BlocksTableItem,BlocksList,BlocksListItem}.tsx;src/shared/lists/DataList.tsx. Replace theDataListplaceholders with the page's entity (don't leave the generic "items"): setemptyTextto a plural no-data sentence ("There are no blocks.") andemptyStateProps={{ term: '<entity>' }}to the singular form ('block').
Then branch on the Step 0 layout decision:
A. No tabs
Use the single page shell that embeds its one content body directly:
- general →
general/Page.tsx.tmpl(content inline; no separate body needed). - detail →
detail/Page.tsx.tmpl(reads the route param, rendersPageTitle+Details) +detail/Details.tsx.tmpl. - index →
index/Page.tsx.tmpl(rendersPageTitle+Content) +index/{Content,Table,TableItem,List,ListItem}.tsx.tmpl.
B. With tabs
- Page shell — copy
tabs/PageWithTabs.tsx.tmpl(→__PageName__.tsx, directly in the page dir). It rendersPageTitle+RoutedTabs. Fill thetabsarray with one{ id, title, component }entry per tab; the first tab's id is conventionally'index'. If adetailstab exists it is the first entry (id: 'index', titleDetails). - One body per tab — for each tab, create a kebab-case sub-folder under the page dir and scaffold a
body there from the content template matching its type (
Details/ indexContent+ sub-components /GeneralInfo), named after the tab (__PageName__<TabName>, see the folders & naming rule above). Wire the renamed components into the shell'stabsarray and imports.
Leave the // TODO (api-data): … markers in place — data fetching belongs to the wiring phase (Step 10).
Step 3 — Route file + server guard
The guard and the route belong together: the route's getServerSideProps references the guard.
Server guard (config-gated pages) — in
src/server/getServerSideProps/guards.tsadd:export const __gsspName__: Guard = (chainConfig: typeof config) => async() => { if (!chainConfig.features.__featureName__.isEnabled) { return { notFound: true }; } };For a core slice page, check
chainConfig.slices.__featureName__.isEnabledinstead. Then register the factory insrc/server/getServerSideProps/main.ts:export const __gsspName__ = factory([ guards.__gsspName__ ]);An un-gated page needs no new guard — reuse
base(ornotMultichainfor non-multichain pages) as__gsspName__and skip the guard creation.Route file — create
src/pages/__route__.tsxfromtemplates/route.tsx.tmpl. For a dynamic route use the commented dynamic-route variant in the template (threadsquerythroughPageNextJs; exemplarsrc/pages/tx/[hash].tsx).
Step 4 — Generate route types
Run pnpm run routes:generate (runs nextjs-routes) to regenerate src/shared/router/nextjs-routes.d.ts.
__pathname__ only type-checks after this, so do it before the steps below.
Step 5 — Navigation
In src/shell/navigation/useNavItems.ts add a NavItem to the group agreed in Step 0:
const __featureName__: NavItem | null = config.features.__featureName__.isEnabled ? {
text: '__title__',
nextRoute: { pathname: '__route__' as const },
// TODO (design): set nav icon
isActive: pathname.startsWith('__route__'),
} : null;
For a core slice page drop the config.features.* guard (or gate on config.slices.*). Do not invent an
icon — leave the // TODO (design): set nav icon comment and omit the icon field so the user assigns the correct
sprite later. Then insert the item into the appropriate group array.
Step 6 — Metadata
In src/shell/metadata/templates/index.ts add an entry keyed by __pathname__, using the user-approved
title/description (default only, no enhanced):
'__pathname__': {
metadata: {
title: { 'default': '…' },
description: { 'default': '…' },
},
// og below — INDEX pages only
og: OG_ROOT_PAGE,
},
og: OG_ROOT_PAGE is for index pages only. Detail and general pages omit the og field entirely
(matches existing detail entries such as /block/[height_or_hash]).
Step 7 — Page-type analytics
In src/services/mixpanel/get-page-type.ts add an entry to PAGE_TYPE_DICT (a
Record<Route['pathname'], string>). Suggest a human-readable name; let the user adjust:
'__pathname__': '__title__',
Step 8 — Sitemap (static routes only)
If the route is dynamic, skip this step — dynamic routes are required API data and therefore are out of the scope of this workflow.
For a static, config-gated route, add a case to the transform switch in
deploy/tools/sitemap-generator/next-sitemap.config.js so it's excluded when the feature is off:
case '__route__':
if (process.env.NEXT_PUBLIC_<FEATURE>_ENABLED !== 'true') {
return null;
}
break;
Step 9 — Check TypeScript compiles
Run pnpm run lint:tsc (tsc -p ./tsconfig.json) to confirm the new files type-check. The scaffold uses the
unknown item type and never reads properties off it, so TypeScript should pass as-is. ESLint is expected
to fail on the scaffold's intentional TODOs (unused item props, single-value consts) until the data
is wired (Step 10, or deferred follow-up work) — don't gate on it at this stage.
Step 10 — Wire data (if agreed in Step 0)
If Step 0 settled on wiring data now, read wiring.md (next to this file) and follow it. This step is
done when its verify step (W5) passes.
Direct entry — for a page scaffolded earlier (its TODO (api-data): markers are still in place), skip
Steps 1–9: run only the Data item of the Step 0 interview, then start at wiring.md.