page-research-tools
Spec for the interactive research and visualization pages on sahin.io/angel-investments (/research, /company-compare, /competitor-radar, /network-graph, /market-timing, /landscape, /timeline, /map). Use when building, rebuilding or changing any of these tools, their charts, their copy or the research data they read.
- Source file
.claude/skills/page-research-tools/SKILL.md- Routes covered
/research/company-compare/competitor-radar/network-graph/market-timing/landscape/timeline/map
Research and visualization tools
Routes
| URL | React kind | Menu section |
|---|---|---|
| https://sahin.io/angel-investments/research | research (menu label "Matrix") |
research |
| https://sahin.io/angel-investments/company-compare | companyCompare |
research |
| https://sahin.io/angel-investments/competitor-radar | competitorRadar |
research |
| https://sahin.io/angel-investments/network-graph | networkGraph |
research |
| https://sahin.io/angel-investments/market-timing | marketTiming |
research |
| https://sahin.io/angel-investments/landscape | landscape |
insights |
| https://sahin.io/angel-investments/timeline | timeline |
insights |
| https://sahin.io/angel-investments/map | map |
insights |
Eight routes, all kind legacyFeature: content/shared/navigation.yaml → LEGACY_MENU_PAGES (shared/publicNavigation.ts) → createSeoRouteManifest() (server/seoRouteManifest.ts). renderLegacyFeaturePage() writes the static shell; scripts/generate-static.ts replaces the body with the React page from reactPageForRoute() (server/publicReactPages.ts). Exactly one /company-compare route: no pairwise "-vs-" pages. URL rules: public-url-conventions.
Files
| Page | Props builder | Component / types | CSS | Copy |
|---|---|---|---|---|
| /research | buildResearchProps in server/publicPages/research.ts |
client/src/publicPages/ResearchPage.tsx, researchTypes.ts |
styles/research.css |
content/pages/research.yaml |
| /company-compare | buildCompanyCompareProps in server/publicPages/companyCompare.ts |
CompanyComparePage.tsx, companyCompareTypes.ts |
styles/companyCompare.css |
content/pages/company-compare.yaml (incl. sections = table rows) |
| /competitor-radar | buildCompetitorRadarProps in server/publicPages/competitorRadar.ts |
CompetitorRadarPage.tsx, competitorRadarTypes.ts |
styles/competitorRadar.css |
content/pages/competitor-radar.yaml |
| /network-graph | buildNetworkGraphProps in server/publicPages/networkGraph.ts |
NetworkGraphPage.tsx, networkGraphTypes.ts |
styles/networkGraph.css |
content/pages/network-graph.yaml (incl. backgroundGroups) |
| /market-timing | buildMarketTimingProps in server/publicPages/marketTiming.ts |
MarketTimingPage.tsx, marketTimingTypes.ts |
styles/marketTiming.css |
content/pages/market-timing.yaml (incl. conditions per year) |
| /landscape | buildLandscape in server/publicReactPages.ts |
LandscapePage.tsx, LandscapeCompany in types.ts |
client/src/index.css (.ls-*, .tl-filters) |
content/pages/landscape.yaml |
| /timeline | buildTimelineEvents, parseRoundDate in server/publicReactPages.ts |
TimelinePage.tsx, TimelineEvent in types.ts |
client/src/index.css (.tl-*) |
content/pages/timeline.yaml (TIMELINE_SHAPE; events titles) |
| /map | buildMapCompanies in server/publicReactPages.ts |
MapPage.tsx (d3 + topojson, lazy client/public/world-110m.json), MapCompany in types.ts |
client/src/index.css (.gm-*, .tl-*) |
content/pages/map.yaml |
- Kinds registered in
client/src/publicPages/types.ts,components.client.ts,components.server.ts; dedicated CSS files are@imported fromclient/src/index.css. - Builders read copy with
loadPageContent()(server/pageContent.ts); components import@/generated/copy/pages/<file>(regenerate withnode scripts/sync-site-copy-module.mjs). - Data:
ctx.companies; research records vialoadEnrichedRecords()(client/public/enriched-company-data.json); locations fromclient/public/company-locations.json(/map); the sourced company value viasourcedValueUsd()/sourced()(server/publicCompanyValue.ts); logos viapublicCompanyLogo().
Sections on the page
- /research: source line → tabs grouping every company by $1B+ (sourced), market, status, latest round (
roundOrder), Series A vs Series B, year started → company rows. - /company-compare: heading + intro → picker (search, add, remove; up to ten; opens on
DEFAULT_SLUGSanthropic, spacex, scale-ai, cerebras-systems) → sector overlap → comparison table, sections and rows from YAML, one display string per cell ("—" when unknown or blocked). - /competitor-radar: header → stats → most competitive markets → search → per-company rows with up to five direct competitors.
- /network-graph: header → stats → search and filter views → legend → SVG graph (companies linked by shared co-investors and by founders' shared employers or schools; positions from a seeded force layout on the server, browser adds drag, zoom, hover) → info panel.
- /market-timing: header → stats → controls → yearly heatmap of dated funding rounds by market against
conditions(bull / bear backdrop) → quarterly view → breakdown → insights. Counts companies' public rounds, never the owner's entry dates. - /landscape: header → view toggle (bubble map / treemap) and size-by (company value, funding, team) → sector legend → chart → tip.
- /timeline: header → year chips → search, type and market filters → stats → per-year bar chart → event feed (founded, funding round, IPO, acquisition). Listings and exits have no date in the public data and show as undated, never on an invented date.
- /map: header → search, market filter → world map with city markers and zoom → country list → selected-city panel → top locations.
SEO
- Title and description:
content/shared/navigation.yaml./landscapeand/company-comparehave recovery surfaces incontent/pages/recovery-surfaces.yaml;/company-compareis inMENU_DESCRIPTION_FEATURESso it keeps the menu description,/landscapeuses the recovered hero intro. - JSON-LD:
buildPageGraph({ kind: "legacyFeature", itemList })fromlegacyFeatureContextLinks():/landscapelists Category links,/company-comparethe first six companies, the rest the default Explore + Categories list. - Canonical via
headSpec(); all eight areindexand in the sitemap. Head details:seo-head-and-schema.
Rules
- Values: sourced company value only, with its citation; never research figures. No private deal data, and no owner entry dates (the old Market Timing used them; it now plots public rounds):
private-data-boundary. - Every research string passes
findRestrictedTerms(…, RESTRICTED_LLMS_TERMS)(server/publicContentPolicy.ts) or is dropped / shown as "—". - Research field names are mapped to neutral prop names on the server; the browser never fetches the research files.
- Network layout must stay deterministic (seeded PRNG in
networkGraph.ts) so builds are identical; the owner is excluded as a hub. - Counts:
portfolio-counts-single-source. Copy:page-content-yaml.
Tests
server/company-compare-recovery.test.ts: one canonical route, no "-vs-" pages, same-origin logos, no restricted data.server/public-react-document.test.ts:parseRoundDateformats; timeline does not invent dates;/mapresolves to kindmap.server/public-company-value.test.ts: sourced-only values on/landscape,/map,/research.server/seo-route-manifest.test.ts(/company-compareislegacyFeature, linked from the hub),server/public-site.test.ts,server/legacy-redirects.test.ts.
Rebuild from scratch
- Confirm the eight menu entries in
content/shared/navigation.yaml; runnode scripts/sync-site-copy-module.mjs. - Write the eight
content/pages/*.yamlfiles. - Write the builders (
server/publicPages/*.ts, andbuildLandscape/buildTimelineEvents/buildMapCompaniesinserver/publicReactPages.ts): declare the YAML shape, join research records by slug, apply the content policy and sourced-value rules, precompute layouts on the server. - Add each path to
reactPageForRoute(). - Write components and types, register the kinds in
types.ts,components.client.ts,components.server.ts, add CSS +@import(pattern:new-react-page; theming:theme-system). - Run
deploy-gate, thenui-before-after(desktop and mobile), thenship-to-production.