site-foundation
The base of sahin.io/angel-investments — the stack, the route manifest, the two render paths (static renderers vs React pages), the static build, and the order in which the page skills rebuild the site. Use first when rebuilding the site from scratch, before adding a new route family, or when orienting in how a URL becomes an HTML file.
- Source file
.claude/skills/site-foundation/SKILL.md
Site foundation
What it is
A public, statically generated portfolio site served at https://sahin.io/angel-investments/
(Cloudflare Pages behind the sahin.io proxy; see production-deployment). Every public URL is a
route in one manifest; every route is pre-rendered to HTML at build time and then hydrated by a
React client bundle.
Stack (package.json): React 19, Vite 7, Tailwind 4, wouter (client routing), TypeScript 5.9,
vitest, tsx. Copy lives in YAML/Markdown under content/ (see page-content-yaml).
Files
server/seoRouteManifest.ts—createSeoRouteManifest()builds every route ({ path, canonicalUrl, file, kind, title, lastModified, indexing });renderRouteDocument()dispatches by kind to the static renderers;renderRouteHtml()wraps it and refuses anoindexroute whose HTML lacks the robots meta.server/publicPortfolio.ts—RouteKindunion: home, explore, legacyFeature, reactPage, newsDetail, company, category, subcategory, companyInvestors, categoryInvestors, subcategoryInvestors, investorsIndex, coInvestor, guidesIndex, guide, about, thesis, pitch, faq, glossary, list, cityList, listsIndex.server/reactPageRoutes.ts— React-only routes:SINGLE_PAGESand slugFAMILIES(/founder,/status,/news,/sectors,/market); listed in the manifest asreactPage.server/publicReactPages.ts—reactPageForRoute(path, ctx)returns{ kind, props }for any route that has a React page (SINGLE_PAGES/FAMILIES first, then route kinds and exact paths).server/publicPages/*.ts— one props builder per React page (build<Name>Props,<name>PageMeta).client/src/publicPages/—*Page.tsx,*Types.ts,types.ts(PublicReactPageunion),PublicReactPageView.tsx,components.server.ts,components.client.ts,styles/*.css.server/publicReactDocument.ts—renderPublicReactDocument()merges static head + React body.client/src/ssr/entry-server.tsx—renderPublicPage(), built byvite.config.ssr.ts.client/src/App.tsx,client/src/main.tsx— the same tree on server and client (hydration).scripts/generate-static.ts— the static build.
Two render paths
- Static renderer.
renderRouteDocument()→renderPublic*Page/renderEditorialDocument/renderListRoute/renderLegacyFeaturePage/renderReactShellPage, each ending inrenderDocument(ctx, headSpec(...), breadcrumbs, body). This HTML always exists: it owns the<head>(title, canonical, OG, JSON-LD — seeseo-head-and-schema) and the crawlable body. - React page. If
reactPageForRoute()returns a page,renderPublicReactDocument()keeps the static head, replaces the static header with the server-rendered React layout (App→NavBar+PublicReactPageView), and passes the static breadcrumb and footer in asServerChrome { crumbHtml, footerHtml, articleHtml? }. Kinds inEMBED_SERVER_BODY_KINDS(companyView,companyInvestorsView,categoryInvestorsView,firmView) also keep the static body below the React part; editorial routes keep their<article class="editorial">. With no React page, the static body is wrapped inServerPageBody(.public-page).
reactPage routes have only a shell static body (renderReactShellPage: H1 = crumb, intro =
description); the React page replaces it. Menu pages (legacyFeature) come from
content/shared/navigation.yaml (see site-chrome-navigation).
The hub (/, kind home) is not rendered by the dispatcher: renderPublicHubDocument() fills
the Vite shell's <!--PUBLIC_HEAD--> / <!--PUBLIC_INDEX--> markers (see page-hub-home).
The build (scripts/generate-static.ts)
Run after npx vite build --config vite.config.cloudflare.ts (CI does both; see
.github/workflows/deploy-cloudflare.yml):
- Vite SSR build of
client/src/ssr/entry-server.tsx→dist/ssr-entry/. - Read
dist/cloudflare/index.html(must still have both markers); write the themed CSS asset (buildThemedCssAsset); copy the allowlisted public assets (favicon, robots.txt, images). - Load portfolio, investor research, editorial (
status: approvedonly), taxonomy content;createSeoRouteManifest();createRenderContext()(one sharedfacts). - For each non-home route:
renderRouteHtml→assertPublicHtml→ raw-source-URL checks →renderPublicReactDocument→ everyNAVIGATION_HREFSlink must be present → writedist/cloudflare/ssr/<route.file>. - Hub
index.html,seo-route-manifest.json,source-reference-map.json,sitemap.xml(buildCanonicalSitemap),legacy-redirect-map.json,llms.txt,llms-full.txt.
Local: npx vite build --config vite.config.cloudflare.ts && NODE_ENV=development npx tsx scripts/generate-static.ts.
Local screenshots need the prefix proxy (see ui-before-after).
Rebuild order (all skills)
site-foundation(this) — manifest, dispatcher, render paths, build.site-chrome-navigation— header, sidebar, mobile tabs, footer, breadcrumbs, menu YAML.seo-head-and-schema— head tags, titles, canonical, JSON-LD, sitemap, llms, robots.theme-system— investor themes, CSS variables, hub views.i18n-locales— the English-only locale setup.new-react-page— the generic recipe every React page follows.- Page families:
page-hub-home→page-company→page-taxonomy→page-investors→page-vc-portfolios→page-editorial→page-angel-guides-reports→page-news→page-founders→page-status-sectors-markets→page-performance→page-research-tools→page-skills-and-agents. - Every change:
deploy-gate→ui-before-after→ship-to-production.
Cross-cutting rules: page-content-yaml (no copy in code), portfolio-counts-single-source,
private-data-boundary, public-url-conventions, project-architecture.
Tests
server/seo-route-manifest.test.ts (route kinds, reactPage routes), server/public-routing.test.ts,
server/public-react-document.test.ts, server/route-indexing.test.ts,
server/public-site.test.ts, server/content-in-yaml.test.ts, server/production-startup.test.ts.
Run them all through deploy-gate (scripts/gate.ts).