A unified front door for entrepreneurship programs across all ten University of California campuses — incubators, accelerators, courses, funding, competitions, and maker spaces in one searchable, comparable catalog.
Built on Vite+ — the unified open-source toolchain that bundles Vite, Vitest, Oxlint, Oxfmt, Rolldown, and tsdown into a single vp CLI.
- React 18 + TypeScript (strict)
- React Router (HashRouter, for static-host deep links)
- CSS variables from the official UC design tokens (Source Serif 4 + Source Sans 3, UC Blue / Dark Blue / Gold, square corners, Dark Blue tinted shadows)
Requires Node.js 24 (the LTS as of early 2026). The crawler scripts run TypeScript natively under node — node scripts/crawl/run.ts only works because Node ≥22 strip-types is the default and explicit .ts import extensions resolve out of the box. Use vp env use 24 (Vite+ ships Node version management) or your usual version manager.
# install Vite+ globally if you haven't
curl -fsSL https://vite.plus | bash
# install deps
vp install
# dev server
vp dev
# format + lint + type-check + duplication scan (fallow)
vp run check
# auto-fix formatting and lint issues
vp check --fix
# tests
vp test
# production build
vp build
# preview production build
vp previewA Git pre-commit hook runs vp run check (format, lint, type-check, and fallow) before every commit, so issues are caught locally instead of in CI. It activates automatically on vp install / pnpm install — the prepare script points core.hooksPath at .githooks/. Bypass a single commit with git commit --no-verify.
src/
├── App.tsx # router + compare provider
├── main.tsx # entry
├── styles/
│ ├── tokens.css # UC design tokens (verbatim from brand guide)
│ └── global.css # resets + body defaults
├── data/
│ ├── types.ts # Campus, Program, ProgramType, Spotlight
│ ├── campuses.ts # 10 UC campuses with map coords, colors, hubUrl
│ ├── types-list.ts # program types, industries, stages, eligibility, durations
│ ├── normalize.ts # slug + canonical type/industry/stage helpers, mergePrograms
│ ├── programs.ts # curated programs + spotlight stories (merges crawled data)
│ └── programs.generated.ts # AUTO: written by `vp run build-data`
├── lib/ # icons, compare cart context, program gradient
├── components/ # Nav, Footer, Page, ProgramCard, CaliforniaMap, etc.
└── pages/ # HomePage, DiscoverPage, ProgramDetail, CampusPage, etc.
scripts/crawl/
├── sites.json # campus seed URLs
├── run.ts # Playwright crawler (worker pool over campuses)
├── extract.ts # link discovery + in-page program extractor
├── build-data.ts # data/crawled/*.json → src/data/programs.generated.ts
└── campuses/ # per-campus override hooks (allowlist, denylist, name filters)
data/crawled/ # one JSON file per campus, written by `vp run crawl`
The catalog is a merge of curated programs (hand-shaped in src/data/programs.ts) and crawled programs (auto-extracted from each campus's innovation hub). On conflict, the curated record wins; the crawled record fills in optional fields the curated copy is missing (website, applicationLink, sourceUrl, lastUpdated, longDescription).
# install Playwright's chromium once
vp install
vp exec playwright install chromium
# crawl every campus (≈ 5–10 min over a fast connection)
vp run crawl
# scope to a subset
vp run crawl -- --campus=berkeley,la --limit=10
# walk the seed but skip extraction
vp run crawl -- --campus=sd --dry-run
# regenerate src/data/programs.generated.ts from data/crawled/*.json
vp run build-data
# the combined shortcut
vp run refresh-dataWeekly automation: .github/workflows/weekly-crawl.yml runs refresh-data every Sunday and commits the regenerated module. Trigger it on demand from the Actions tab via Run workflow.
Required fields stay stable for back-compat with the design prototype's curated cards:
interface Program {
id: string; // also acts as the URL slug
slug?: string; // explicit slug (defaults to id)
name: string;
campus: string; // CAMPUSES.id
type: string; // canonical type (TYPES.id)
desc: string; // card-length summary
industries: string[]; // canonical industry labels
stage: "Idea" | "Prototype" | "Pre-seed" | "Scaling";
eligibility: string[];
duration: string;
funding: string;
selectivity: string;
cohortSize: number | null;
deadline: string;
// Optional, filled in by the crawler when available:
longDescription?: string;
deadlines?: { label?: string; date?: string }[];
website?: string;
applicationLink?: string;
associatedCenter?: string;
tags?: string[];
lastUpdated?: string; // ISO 8601, set by the crawler
sourceUrl?: string; // page the program was extracted from
featured?: boolean;
eyebrow?: string;
}canonicalType / canonicalIndustries / canonicalStage in src/data/normalize.ts map noisy upstream phrasing ("startup accelerator", "AI/ML", "machine learning") onto the design's controlled vocabulary.
- Append the campus to
src/data/campuses.ts(id, color, map x/y,hubUrl). - Add the matching entry to
scripts/crawl/sites.json. - Drop
scripts/crawl/campuses/<id>.mjsre-exporting from_default.mjs. OverridelinkAllowlist,linkDenylist,allowName, ormaxSubpagesonly when the defaults pull in junk. - Run
vp run refresh-data.
If a campus's site is JS-heavy and the generic extractor returns nothing, the override module is the right place to bolt in a custom discover selector — keep that surgery campus-local.
| Path | Page |
|---|---|
/ |
Home |
/discover |
Program discovery (with ?q=, ?campus=, ?type=, etc.) |
/program/:id |
Program detail |
/campus/:id |
Campus page |
/campuses |
All campuses index |
/compare |
Comparison tool |
The site has two deploy targets:
GitHub Actions workflow at .github/workflows/deploy.yml builds on every push to main and publishes to GitHub Pages. The workflow:
- Installs Vite+ globally
- Runs
vp run check(format + lint + type-check + duplication scan) - Runs
vp buildwithVITE_GH_PAGES=1so assets resolve under/uc-entrepreneurship-hub/ - Copies
index.htmlto404.htmlso hash-router deep links work - Uploads
dist/and deploys
To enable: in the GitHub repo settings, Settings → Pages → Source: GitHub Actions.
Cloudflare auto-deploys every branch push (including PRs) to its own preview URL. The static-asset deploy is configured by the wrangler.jsonc at the repo root — that file is the source of truth for how the Worker serves dist/. Connect the repo from the Cloudflare dashboard once and every PR gets a unique preview URL.
Build settings (Cloudflare dashboard → your project → Settings → Build):
| Field | Value |
|---|---|
| Build command | pnpm run build |
| Deploy command | npx wrangler versions upload (default for Workers projects, leave as-is) |
| Root directory | (empty) |
The Worker's name in wrangler.jsonc ("name": "uc-entrepreneurship-hub") must match your Cloudflare project name. Update either the file or the dashboard until they agree.
Environment variables (Settings → Environment variables, set on both Production + Preview):
| Name | Value |
|---|---|
NODE_VERSION |
24 |
PNPM_CONFIG_OPTIONAL |
false |
PNPM_CONFIG_OPTIONAL=false skips the optionalDependencies block in package.json — playwright lives there because only the crawler needs it, and it's ~70MB of binary that the static build never touches. Skipping it cuts CF install time noticeably without affecting the build.
Notes:
- Don't set
VITE_GH_PAGES=1on Cloudflare — it's only for the GH Pages base path. CF Pages serves at/, which isvite.config.ts's default. - The build command must be
pnpm run build, notvp builddirectly. Cloudflare's build shell doesn't putnode_modules/.binon$PATH; going through the npm script does. - The app uses
HashRouter, so deep links work without a_redirectsfile or SPA fallback — every URL servesindex.htmland the fragment is read client-side.
PR preview URLs surface automatically as a comment on the PR (configurable in the CF dashboard).
- Design system: UC Brand Guide
- Design prototype: handed off from Claude Design
- Toolchain: Vite+