The public website, blog and documentation for Uncava — executive search and talent mapping. The application lives at app.uncava.com in a separate repository.
Proprietary. © Uncava. All rights reserved.
- Astro — static output, TypeScript
strictest - Starlight — documentation under
/docs, with Pagefind search - Tailwind CSS v4 via
@tailwindcss/vite; tokens insrc/styles/theme.css - Geist and Geist Mono, self-hosted via Fontsource
- Build-time Open Graph images with Satori + Sharp
- Sveltia CMS at
/adminfor non-technical editors - Firebase Hosting; GitHub Actions for CI, deploys and preview channels
- Vitest, ESLint, Prettier,
astro check, Lighthouse CI
Design source of truth: design/*.dc.html. Brand assets: brand/.
Requires Node 24 (see .nvmrc) and npm 11.
nvm use
npm ci
npm run dev # http://localhost:4321| Command | What it does |
|---|---|
npm run dev |
Dev server with hot reload (drafts visible) |
npm run build |
Static build into dist/ |
npm run preview |
Serve dist/ locally |
npm run check |
astro check and tsc |
npm run lint / npm run format |
ESLint / Prettier (write) |
npm test |
Vitest |
npm run check:csp |
Audit dist/ against the CSP in firebase.json |
npm run verify |
Everything CI runs except Lighthouse |
npm run lhci |
Lighthouse CI against dist/ (needs Chrome) |
Optional build-time environment variables (copy into .env, never commit it):
| Variable | Effect |
|---|---|
PUBLIC_DEMO_FORM_ENDPOINT |
The /contact form posts here. Unset: the form shows the contact email instead of a submit button. Add the endpoint's origin to form-action in firebase.json. |
PUBLIC_NEWSLETTER_FORM_ENDPOINT |
The blog's newsletter form posts here. Unset: a placeholder is shown. Same form-action rule. |
Editors don't need a local setup.
- Open https://uncava.com/admin and sign in with GitHub. Your GitHub account needs write access to
nextwebspark/uncava-website. - Choose Blog posts, Authors, or a Docs section. Create or edit an entry.
- Save opens a pull request (editorial workflow). A preview link is posted on it.
- When it's reviewed, set it to Ready and Publish. The merge deploys the site.
Rules for images, alt text and fields are shown in the editor as hints. Developers: the CMS config and
the content schemas must change together — see .claude/skills/content-cms/SKILL.md.
GitHub sign-in in the CMS needs a small OAuth relay, the Sveltia CMS Authenticator on Cloudflare Workers:
- Deploy the worker (its README has a one-click deploy, or
wrangler deploy). - Create a GitHub OAuth App: homepage
https://uncava.com, authorization callbackhttps://<worker-host>/callback. - Set worker variables
GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET(encrypted), andALLOWED_DOMAINS=uncava.com. - Replace
https://[SVELTIA_AUTH_WORKER_HOST]inpublic/admin/config.yml(backend.base_url) with the worker URL.
Merging to main deploys nothing — CI proves the site is releasable and stops there. Production changes
only through a release. Same-repository pull requests still get a 7-day preview channel from
preview.yml.
GitHub → Actions → Release → Run workflow, choosing patch, minor or major.
- It refuses unless CI is green on the tip of
main(forceoverrides). - It takes the latest
vX.Y.Ztag, bumps the chosen part (the first release isv0.1.0), pushes the tag and publishes a GitHub Release whose notes are the merged pull requests since the last one. - It calls Deploy with that tag:
npm run verifyon the tag,firebase deploy --only hostingwith the version as the release message, then a smoke test against the live site.
The tag is the version; package.json is not bumped. What is live is always one request away:
https://uncava.com/version.json → {"version","commit","deployedAt"}.
GitHub → Actions → Deploy → Run workflow with an existing tag, e.g. v0.3.0. It rebuilds that tag
and ships it; no new version is cut. For an instant rollback without a rebuild, use Firebase console →
Hosting → release history → Rollback, then redeploy the tag so version.json agrees.
The site lives in the hak-talent-mapping project as its own Hosting site (hosting.site in
firebase.json), deployed by a keyless Workload Identity Federation identity. All of it is created by
one idempotent, re-runnable script:
gcloud auth login
./scripts/gcp-bootstrap.sh # GCP_PROJECT / GITHUB_REPO override the defaultsIt enables the APIs, adds Firebase to the project, creates the Hosting site, the
uncava-website-deployer service account (roles/firebasehosting.admin and
roles/serviceusage.serviceUsageConsumer only) and a WIF provider pinned to this repository, then
prints the gh variable set commands for GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT,
FIREBASE_PROJECT_ID and, once the domain is connected, PUBLIC_BASE_URL. No secrets are needed.
Until those variables are set, Deploy and Preview skip with a notice.
By hand, once: the production environment (required reviewer, deployments from main only) and a tag
ruleset on v* blocking update and deletion.
- In the Firebase console → Hosting → Add custom domain, add
uncava.com, thenwww.uncava.comset to redirect touncava.com. - In Cloudflare DNS, create exactly the records Firebase shows (TXT for verification, then A/AAAA).
- Set every one of them to DNS only (proxy off, grey cloud). Firebase issues the TLS certificate and serves the security headers; Cloudflare's proxy would block certificate issuance and override caching.
- Wait for Firebase to report the certificate as active before relying on HSTS preload.
main is expected to require: a pull request, one approval from a code owner (.github/CODEOWNERS),
the CI status check passing, branches up to date before merge, and no force pushes or deletions.
Content edits from the CMS follow the same rules.
See CLAUDE.md for the layout table, invariants and conventions, and .claude/skills/ for the detail
behind each area.
See SECURITY.md.