sahin.io

All skills and agents

page-hub-home

The portfolio home page (the hub) and the /explore site directory on sahin.io/angel-investments. Use when building, rebuilding or changing the hub's interactive React view, its crawlable no-JS index, its head/JSON-LD, or the /explore directory.

Source file
.claude/skills/page-hub-home/SKILL.md
Routes covered
//explore

Hub home and Explore directory

Routes

URL Route kind Render path
https://sahin.io/angel-investments/ home (1 route) Vite SPA shell + static index injected at build
https://sahin.io/angel-investments/explore explore (1 route) static only (renderExploreDirectory)
  • Both are pushed first in createSeoRouteManifest() (server/seoRouteManifest.ts): route("/", "home", titles.home) and route(EXPLORE_PATH, "explore", titles.explore). Titles come from content/pages/site-titles.yaml via titleCopy().
  • renderRouteDocument() returns null for home: the hub is NOT rendered by the route dispatcher. scripts/generate-static.ts reads the Vite-built dist/cloudflare/index.html (the shell) first, renders every other route, then calls renderPublicHubDocument(...) and overwrites index.html.
  • explore is a reserved top-level segment (RESERVED_TOP_LEVEL_SEGMENTS in shared/publicUrls.ts), so no company or Category can take it.

Files

  • Hub document: renderPublicHubDocument, renderPublicHubIndex, hubPageMetadata, renderHubEditorialNav, HUB_INDEX_CSS, HUB_HEAD_MARKER (<!--PUBLIC_HEAD-->), HUB_INDEX_MARKER (<!--PUBLIC_INDEX-->), localLogoResolver — all in server/seoRouteManifest.ts.
  • Hub shell: client/index.html must contain <div id="root"></div> immediately followed by <!--PUBLIC_INDEX-->, exactly one of each marker, and NO hardcoded <title>, canonical, og:* or JSON-LD (the function throws otherwise).
  • Interactive hub: client/src/App.tsx (HumanRouter routes only / → Home, everything else → NotFound), client/src/pages/Home.tsx (hero, stats, thesis card, public companies, acquired exits, deck CTA, toolbar with search/filters/sort, view modes, empty state, directory, notice, related links). View components live in client/src/components/views/*View.tsx; investor themes swap in THEME_VIEWS (see theme-system).
  • Copy: content/chrome/hub.yaml (interactive hub words; browser reads client/src/generated/copy/chrome/hub produced by node scripts/sync-site-copy-module.mjs); content/pages/portfolio-index.yaml (portfolioIndexCopy() in server/staticPageCopy.ts: title, h1, description, intro, chips, dataset); content/pages/explore.yaml (exploreCopy()).
  • Data: client/public/portfolio-data.json (via loadPublicPortfolio), data/public-investor-research.json, data/entity-facts.json → loadEntityFacts() (facts.derived: totals, exitSplit, categories, subcategories, featuredCompanies, asOfLabel), logos via client/src/generated/logoMap.json + client/public/images/logos/.
  • Static CSS: HUB_INDEX_CSS (inline <style> inside #public-index, reads --investor-* theme vars); Explore uses .explore-* rules in PUBLIC_PAGE_CSS (server/publicChrome.ts).
  • Shared list filter: renderCompanyListControls / renderCompanyListEmpty / companyListItemAttrs (server/companyListControls.ts, words in content/pages/company-list-controls.yaml).

Sections on the page

Hub (/) — top: React Home (NavBar + hero, stats, thesis, public/acquired rows, deck CTA, toolbar, selected view, directory, notice, related). Below the React root, the static #public-index:

  1. <h1> from hubPageMetadata().h1 (the only static h1) and intro (copy.intro filled with totals, exit split, Category/Sub-Category counts, verified-investor count, recognition sentence, deck mailto from mailtoDeckHref).
  2. Categories chips (#categories), Sub-Categories chips.
  3. Investors: link to /angel-investors + chips for every eligible Category/Sub-Category investor directory (investorCollections).
  4. Guides and editorial chips (renderHubEditorialNav, only when the manifest has editorial routes).
  5. All companies list with filter controls (#pi-company-list).

Explore (/explore) — <article class="explore-index">: header (eyebrow, h1, computed description, search input) → Companies (sorted by sortByMarketCap, each with an "investors" secondary link when verified) → Categories + Sub-Categories (with investor-directory links when that route exists) → Investors (every coInvestor route) → Research (legacyFeature, thesis, lists, guides routes) → React page groups from renderExploreReactPages (guides, founder pages, statuses, sectors, markets, news) → inline client search script toggling [data-explore-item].

SEO

  • Hub: title/description from hubPageMetadata(facts); canonical = the hub route's canonicalUrl; one JSON-LD graph from buildPageGraph({ kind: "home", itemList: Categories, dataset: {...} }) (server/publicSchema.ts) rendered through renderHeadMeta(headSpec(...)). Build asserts exactly one <title>, one canonical, one JSON-LD graph, one static <h1>.
  • Explore: title from site-titles.yaml; description filled from explore.yaml with live counts; buildPageGraph({ kind: "explore", itemList: companies }) → CollectionPage (pageEntityType); breadcrumbs hub → Explore. Both indexable, both in the sitemap (buildCanonicalSitemap) and llms files (server/llmsText.ts). Head/schema details: seo-head-and-schema.

Rules

  • Every count (total, live/public/acquired/failed, Categories) comes from facts.derived / shared/portfolioStatus.ts — portfolio-counts-single-source. Never type a number into YAML.
  • The single portfolio return figure (if shown in the hub) comes only from data/portfolio-performance.yaml via {irr}; no per-company figures — private-data-boundary.
  • No visible words in code — page-content-yaml. URLs written in full — public-url-conventions.
  • HumanRouter must stay limited to /; static pages are separate documents (keeps private legacy views out of client navigation).
  • Theme views copy a firm's layout, never its logo (theme-system).

Tests

server/seo-route-manifest.test.ts, server/public-schema.test.ts, server/public-link-graph.test.ts, server/editorial-content.test.ts, server/public-react-document.test.ts, server/content-in-yaml.test.ts, server/portfolio-status-single-source.test.ts, server/keyword-[redacted].test.ts, server/legacy-redirects.test.ts.

Rebuild from scratch

  1. Keep the two route(...) entries at the head of createSeoRouteManifest() and the case "home": return null / case "explore" branches in renderRouteDocument().
  2. Write content/pages/portfolio-index.yaml, content/pages/explore.yaml, content/chrome/hub.yaml (placeholders like {total} only — no literal counts); run node scripts/sync-site-copy-module.mjs.
  3. Ensure client/index.html has the two markers and no head tags it must not own.
  4. Implement hubPageMetadata → renderPublicHubIndex → renderPublicHubDocument (head via headSpec/buildPageGraph, marker replacement, the four count assertions).
  5. Wire scripts/generate-static.ts: read the shell before rendering routes, write the hub last.
  6. Build Home.tsx from @/data/portfolioData + generated hub copy; register theme views per theme-system.
  7. Implement renderExploreDirectory + renderExploreReactPages using manifest route kinds, not hardcoded paths.
  8. Run deploy-gate → ui-before-after (desktop + mobile, hub and /explore) → ship-to-production.