sahin.io

All skills and agents

page-performance

Spec for the stage and exit pages on sahin.io/angel-investments (/stages, /exit-readiness, /exit-scenarios, /investment-flow, /secondary-market), including the single general portfolio return figure rule. Use when building, rebuilding or changing any of these pages, their scoring models, their copy or the research data they read.

Source file
.claude/skills/page-performance/SKILL.md
Routes covered
/stages/exit-readiness/exit-scenarios/investment-flow/secondary-market

Stage and exit pages

Routes

URL Kind React kind Menu section
https://sahin.io/angel-investments/stages legacyFeature stages explore
https://sahin.io/angel-investments/exit-readiness legacyFeature exitReadiness research
https://sahin.io/angel-investments/exit-scenarios legacyFeature exitScenarios research
https://sahin.io/angel-investments/investment-flow legacyFeature investmentFlow research
https://sahin.io/angel-investments/secondary-market legacyFeature secondaryMarket research

Five routes, all menu pages: content/shared/navigation.yaml → LEGACY_MENU_PAGES (shared/publicNavigation.ts) → createSeoRouteManifest() (server/seoRouteManifest.ts). renderLegacyFeaturePage() writes a static shell (head, breadcrumb, H1 = menu label, intro); scripts/generate-static.ts swaps in the React page from reactPageForRoute() in server/publicReactPages.ts. URL rules: public-url-conventions.

Files

Page Props builder Component / types CSS Copy
/stages buildStagesProps in server/publicPages/stages.ts (also normalizeStage, parseAmount) client/src/publicPages/StagesPage.tsx, stagesTypes.ts styles/stages.css content/pages/stages.yaml
/exit-readiness buildExitReadinessProps, scoreCompany in server/publicPages/exitReadiness.ts ExitReadinessPage.tsx, exitReadinessTypes.ts styles/exitReadiness.css content/pages/exit-readiness.yaml
/exit-scenarios buildExitScenariosProps, modelExitPaths in server/publicPages/exitScenarios.ts ExitScenariosPage.tsx, exitScenariosTypes.ts styles/exitScenarios.css content/pages/exit-scenarios.yaml
/investment-flow buildInvestmentFlowProps in server/publicPages/investmentFlow.ts (layout with d3-sankey on the server) InvestmentFlowPage.tsx, investmentFlowTypes.ts styles/investmentFlow.css content/pages/investment-flow.yaml
/secondary-market buildSecondaryMarketProps in server/publicPages/secondaryMarket.ts SecondaryMarketPage.tsx, secondaryMarketTypes.ts styles/secondaryMarket.css content/pages/secondary-market.yaml
  • Kinds registered in client/src/publicPages/types.ts (PublicReactPage union), components.client.ts, components.server.ts; CSS @imported from client/src/index.css.
  • Copy: builders call loadPageContent() (server/pageContent.ts) with a declared shape so the YAML is checked at build time; components import @/generated/copy/pages/<file> (regenerate with node scripts/sync-site-copy-module.mjs).
  • Data: ctx.companies; research records from loadEnrichedRecords() (client/public/enriched-company-data.json); /stages also reads client/public/vc-data.json; /secondary-market reads client/public/secondary-market-data.json (server-side only, never fetched by the browser); the sourced company value from sourcedValueUsd() / sourced() (server/publicCompanyValue.ts).

Sections on the page

  • /stages: header → view switch (views) → stage overview (company counts, average funding, status per stage, Seed to Public) → head-to-head stage comparison → per-company cards with a funding journey grouped by stage (journey, card).
  • /exit-readiness: header + model note → the five dimensions (Company Value, Growth, Team, Market Position, Stage Maturity) → stats → grade distribution (A+ to D) → search, grade filter, sort → one row per company (or empty). Only companies with exitStatus === 'Live'.
  • /exit-scenarios: header + model note → stats → search and market filter → legend → per-company IPO / Acquisition / Secondary likelihoods and readiness score (modelExitPaths), table or card view.
  • /investment-flow: header → summary tiles → sankey (all companies → Failed / under $100M or not published / $100M+ → ... → $1B+ → $100B+ → IPO) as precomputed SVG paths → node detail and tooltip.
  • /secondary-market: header + as-of date → stats → search → table of pre-IPO companies (platforms, volume, expected IPO window, sourced company value with citation) → note.

SEO

  • Title and description: content/shared/navigation.yaml. /stages is in MENU_DESCRIPTION_FEATURES, so it always uses the menu description; the others have no recovery surface and also use it.
  • JSON-LD: buildPageGraph({ kind: "legacyFeature", itemList }); the item list is the default from legacyFeatureContextLinks() (Explore link + first six Categories).
  • Canonical via headSpec(); all five are index and in the sitemap. Head details: seo-head-and-schema.

Rules

  • One return figure only. The site may show one general portfolio IRR and nothing else of that kind. Its only source is data/portfolio-performance.yaml (generalIrr), read via GENERAL_IRR in shared/portfolioPerformance.ts or the {irr} placeholder in copy. Never write the number into code or YAML, never compute or show a per-company or per-deal figure, never a second portfolio figure. None of these five pages shows it today; if one should, use {irr}. server/general-irr-single-source.test.ts fails on any other copy or any percentage next to "IRR". Rule text: private-data-boundary.
  • No private deal data (amounts, dates, entities, terms) anywhere: private-data-boundary. These pages describe the companies, never the owner's positions.
  • Company values: sourced only, with citation. A company without one scores 0 on the value dimension (/exit-readiness) and sits in the "not published" tier (/investment-flow). /secondary-market must not pass implied prices, 90-day change or last-round figures, and drops companies listed as Public or Acquired.
  • Research text goes through findRestrictedTerms(…, RESTRICTED_LLMS_TERMS) (server/publicContentPolicy.ts) and is dropped when it trips the policy.
  • Research field names stay on the server: map them to neutral prop names in the builder.
  • Counts and status filters: portfolio-counts-single-source. The /secondary-market menu description hard-codes a company count; keep it in sync or reword it.
  • All words in YAML: page-content-yaml.

Tests

  • server/public-company-value.test.ts: sourced-only values on /stages, /exit-readiness, /investment-flow, /secondary-market; secondary table limited to pre-IPO companies with no unsourced price change.
  • server/general-irr-single-source.test.ts: the single return figure.
  • server/seo-route-manifest.test.ts (manifest, restricted terms), server/public-site.test.ts.

Rebuild from scratch

  1. Confirm the five menu entries in content/shared/navigation.yaml; run node scripts/sync-site-copy-module.mjs.
  2. Write the five content/pages/*.yaml files (labels, model notes, dimension names, column headers).
  3. Write each builder in server/publicPages/: declare the YAML shape, load research records, filter companies, compute scores or layout on the server, map to neutral props, apply the content policy and sourced-value rules.
  4. Add each path to reactPageForRoute() in server/publicReactPages.ts.
  5. Write components and *Types.ts, register kinds in types.ts, components.client.ts, components.server.ts, add CSS + @import (pattern: new-react-page).
  6. Run deploy-gate, then ui-before-after (desktop and mobile), then ship-to-production.