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)androute(EXPLORE_PATH, "explore", titles.explore). Titles come fromcontent/pages/site-titles.yamlviatitleCopy(). renderRouteDocument()returnsnullforhome: the hub is NOT rendered by the route dispatcher.scripts/generate-static.tsreads the Vite-builtdist/cloudflare/index.html(the shell) first, renders every other route, then callsrenderPublicHubDocument(...)and overwritesindex.html.exploreis a reserved top-level segment (RESERVED_TOP_LEVEL_SEGMENTSinshared/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 inserver/seoRouteManifest.ts. - Hub shell:
client/index.htmlmust 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(HumanRouterroutes 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 inclient/src/components/views/*View.tsx; investor themes swap inTHEME_VIEWS(seetheme-system). - Copy:
content/chrome/hub.yaml(interactive hub words; browser readsclient/src/generated/copy/chrome/hubproduced bynode scripts/sync-site-copy-module.mjs);content/pages/portfolio-index.yaml(portfolioIndexCopy()inserver/staticPageCopy.ts: title, h1, description, intro, chips, dataset);content/pages/explore.yaml(exploreCopy()). - Data:
client/public/portfolio-data.json(vialoadPublicPortfolio),data/public-investor-research.json,data/entity-facts.json→loadEntityFacts()(facts.derived: totals,exitSplit, categories, subcategories,featuredCompanies,asOfLabel), logos viaclient/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 inPUBLIC_PAGE_CSS(server/publicChrome.ts). - Shared list filter:
renderCompanyListControls/renderCompanyListEmpty/companyListItemAttrs(server/companyListControls.ts, words incontent/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:
<h1>fromhubPageMetadata().h1(the only static h1) and intro (copy.introfilled with totals, exit split, Category/Sub-Category counts, verified-investor count, recognition sentence, deck mailto frommailtoDeckHref).- Categories chips (
#categories), Sub-Categories chips. - Investors: link to /angel-investors + chips for every eligible Category/Sub-Category investor directory (
investorCollections). - Guides and editorial chips (
renderHubEditorialNav, only when the manifest has editorial routes). - 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'scanonicalUrl; one JSON-LD graph frombuildPageGraph({ kind: "home", itemList: Categories, dataset: {...} })(server/publicSchema.ts) rendered throughrenderHeadMeta(headSpec(...)). Build asserts exactly one<title>, one canonical, one JSON-LD graph, one static<h1>. - Explore: title from
site-titles.yaml; description filled fromexplore.yamlwith 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.yamlvia{irr}; no per-company figures —private-data-boundary. - No visible words in code —
page-content-yaml. URLs written in full —public-url-conventions. HumanRoutermust 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
- Keep the two
route(...)entries at the head ofcreateSeoRouteManifest()and thecase "home": return null/case "explore"branches inrenderRouteDocument(). - Write
content/pages/portfolio-index.yaml,content/pages/explore.yaml,content/chrome/hub.yaml(placeholders like{total}only — no literal counts); runnode scripts/sync-site-copy-module.mjs. - Ensure
client/index.htmlhas the two markers and no head tags it must not own. - Implement
hubPageMetadata→renderPublicHubIndex→renderPublicHubDocument(head viaheadSpec/buildPageGraph, marker replacement, the four count assertions). - Wire
scripts/generate-static.ts: read the shell before rendering routes, write the hub last. - Build
Home.tsxfrom@/data/portfolioData+ generated hub copy; register theme views pertheme-system. - Implement
renderExploreDirectory+renderExploreReactPagesusing manifest route kinds, not hardcoded paths. - Run
deploy-gate→ui-before-after(desktop + mobile, hub and /explore) →ship-to-production.