sahin.io

All skills and agents

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 from client/src/index.css.
  • Builders read copy with loadPageContent() (server/pageContent.ts); components import @/generated/copy/pages/<file> (regenerate with node scripts/sync-site-copy-module.mjs).
  • Data: ctx.companies; research records via loadEnrichedRecords() (client/public/enriched-company-data.json); locations from client/public/company-locations.json (/map); the sourced company value via sourcedValueUsd() / sourced() (server/publicCompanyValue.ts); logos via publicCompanyLogo().

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_SLUGS anthropic, 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. /landscape and /company-compare have recovery surfaces in content/pages/recovery-surfaces.yaml; /company-compare is in MENU_DESCRIPTION_FEATURES so it keeps the menu description, /landscape uses the recovered hero intro.
  • JSON-LD: buildPageGraph({ kind: "legacyFeature", itemList }) from legacyFeatureContextLinks(): /landscape lists Category links, /company-compare the first six companies, the rest the default Explore + Categories list.
  • Canonical via headSpec(); all eight are index and 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: parseRoundDate formats; timeline does not invent dates; /map resolves to kind map.
  • server/public-company-value.test.ts: sourced-only values on /landscape, /map, /research.
  • server/seo-route-manifest.test.ts (/company-compare is legacyFeature, linked from the hub), server/public-site.test.ts, server/legacy-redirects.test.ts.

Rebuild from scratch

  1. Confirm the eight menu entries in content/shared/navigation.yaml; run node scripts/sync-site-copy-module.mjs.
  2. Write the eight content/pages/*.yaml files.
  3. Write the builders (server/publicPages/*.ts, and buildLandscape / buildTimelineEvents / buildMapCompanies in server/publicReactPages.ts): declare the YAML shape, join research records by slug, apply the content policy and sourced-value rules, precompute layouts on the server.
  4. Add each path to reactPageForRoute().
  5. 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).
  6. Run deploy-gate, then ui-before-after (desktop and mobile), then ship-to-production.