sahin.io

All skills and agents

new-react-page

The generic, file-by-file recipe for adding or rebuilding a React page on sahin.io/angel-investments — props builder, *Page.tsx, *Types.ts, CSS, the PublicReactPage union, PublicReactPageView, components.client/server, route registration (menu page, SINGLE_PAGES, FAMILIES or reactPageForRoute), YAML copy, titles, crumbs and tests. Use whenever a new page or slug family is added, an existing route is moved to React, or a page skill says "follow new-react-page".

Source file
.claude/skills/new-react-page/SKILL.md

New React page

Every React page is a pair: a server props builder that computes all data (counts, links, sourced values) from the render context, and a pure component that renders those props with words from YAML. The static document (head, crumb, footer) is still built by the manifest; see site-foundation for the render paths.

Worked examples in the repo

  • Menu page: /lessons — content/shared/navigation.yaml entry → legacyFeature route; reactPageForRoute returns { kind: 'lessons', props: buildLessonsProps(ctx) } by exact path.
  • Single page outside the menu: /about in SINGLE_PAGES (server/reactPageRoutes.ts).
  • Slug family: /status/{slug} in FAMILIES — listStatusDetailSlugs, statusDetailPageMeta, buildStatusDetailProps, crumb from the title.

File checklist (name = camelCase kind, e.g. statusDetail)

  1. Copy content/pages/<page-name>.yaml (schemaVersion: 1, header comment listing placeholders; title/description candidates longest first). Run node scripts/sync-site-copy-module.mjs → client/src/generated/copy/pages/<page-name>.ts. Rules: page-content-yaml.
  2. Types client/src/publicPages/<name>Types.ts — export interface <Name>PageProps; JSON-serialisable only (props are embedded in the HTML for hydration). Comment each field.
  3. Props builder server/publicPages/<name>.ts:
    • build<Name>Props(ctx: RenderContext[, slug]) — data from ctx.companies, ctx.facts, loadEnrichedRecords(), companyHref, publicCompanyLogo, sourced(); counts via summarizePortfolioStatus (portfolio-counts-single-source).
    • Server-side words via loadPageContent('<page-name>', SHAPE) with record/text/list from server/pageContent.ts.
    • For non-menu pages: <name>PageMeta returning { title, description } (title ≤ 60 chars, use fitPageTitle/firstFittingTitle; see seo-head-and-schema).
    • Families: list<Name>Slugs(ctx) and optional <name>Indexing(ctx, slug) → 'noindex' for thin members.
  4. Component client/src/publicPages/<Name>Page.tsx — export default function <Name>Page(props); imports copy from '@/generated/copy/pages/<page-name>' and fillText/fillParts/fillNodes/ NUMBER_LOCALE from @/lib/siteCopy. No literal visible text, no fetch, no reading theme state at render (use tv() / CSS variables; theme-system). One <h1>.
  5. CSS client/src/publicPages/styles/<name>.css, class-prefixed by the page; add @import "./publicPages/styles/<name>.css"; to client/src/index.css.
  6. Union client/src/publicPages/types.ts — import the props type and add | { kind: '<name>'; props: <Name>PageProps } to PublicReactPage.
  7. View client/src/publicPages/PublicReactPageView.tsx — import the props type, add <name>: ComponentType<<Name>PageProps> to PublicPageComponents, add the case '<name>'.
  8. Registries components.server.ts (static import + entry in serverPageComponents) and components.client.ts (<name>: lazy(() => import('./<Name>Page'))). Both must list it or the type check fails.
  9. Route — pick one:
    • Menu page: add to pages + one section in content/shared/navigation.yaml, then add if (routePath === '/<path>') return { kind, props } in reactPageForRoute (server/publicReactPages.ts). Meta description: add to MENU_DESCRIPTION_FEATURES in server/seoRouteManifest.ts if the menu description should be used.
    • Single page outside the menu: add { path, meta, page, lastModified?, schema? } to SINGLE_PAGES and the crumb to content/pages/page-crumbs.yaml.
    • Slug family: add { prefix, crumb, slugs, meta, page, indexing? } to FAMILIES.
    • Restyled existing kind (company, investors): branch on routeKind in reactPageForRoute; add the kind to EMBED_SERVER_BODY_KINDS in server/publicReactDocument.ts to keep the static body. URL and slug rules: public-url-conventions.
  10. Structured data (optional) — schema: ctx => ({ itemList, faqs, dateModified }) on the spec.
  11. Tests — extend the closest existing test: route presence/kind in server/seo-route-manifest.test.ts; React document in server/public-react-document.test.ts; title budget runs automatically in server/keyword-[redacted].test.ts; copy policy in server/content-in-yaml.test.ts and server/site-copy-module.test.ts; links in server/public-link-graph.test.ts; privacy in server/private-deal-data.test.ts.

Rules

  • No private deal data in props, YAML or CSS (private-data-boundary); the one portfolio return figure only from data/portfolio-performance.yaml via {irr}.
  • Company values only as sourced values (SourcedValue, sourcedValueText).
  • Every count computed, never typed into YAML.
  • The page must render with JavaScript off (static HTML is the SSR output) and hydrate without mismatch: no Date.now(), random, window or localStorage during render.

Finish

deploy-gate → ui-before-after (desktop and mobile, via the prefix proxy) → ship-to-production.