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(PublicReactPageunion),components.client.ts,components.server.ts; CSS@imported fromclient/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 withnode scripts/sync-site-copy-module.mjs). - Data:
ctx.companies; research records fromloadEnrichedRecords()(client/public/enriched-company-data.json);/stagesalso readsclient/public/vc-data.json;/secondary-marketreadsclient/public/secondary-market-data.json(server-side only, never fetched by the browser); the sourced company value fromsourcedValueUsd()/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 withexitStatus === '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./stagesis inMENU_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 fromlegacyFeatureContextLinks()(Explore link + first six Categories). - Canonical via
headSpec(); all five areindexand 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 viaGENERAL_IRRinshared/portfolioPerformance.tsor 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.tsfails 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-marketmust 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-marketmenu 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
- Confirm the five menu entries in
content/shared/navigation.yaml; runnode scripts/sync-site-copy-module.mjs. - Write the five
content/pages/*.yamlfiles (labels, model notes, dimension names, column headers). - 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. - Add each path to
reactPageForRoute()inserver/publicReactPages.ts. - Write components and
*Types.ts, register kinds intypes.ts,components.client.ts,components.server.ts, add CSS +@import(pattern:new-react-page). - Run
deploy-gate, thenui-before-after(desktop and mobile), thenship-to-production.