Introduction
Project overview
White Tree Digital (WTD) is the marketing website for a one-person digital-marketing & web-dev studio in Indianapolis, paired with a Sanity studio that the owner edits content in — two independent packages in one monorepo, built around a single goal: turn visitors into qualified leads.
This page is the map of the whole system: what the project is, the two packages and how they relate, and the stack at a glance. For the things this page only points at, follow the links — the monorepo architecture, the two-build split, and the Sanity mental model each go deeper.
What the project is
White Tree Digital, LLC is a solo digital-marketing and web-development studio. The site in this repo is a marketing website, not an application — it exists to generate qualified leads, and every decision is optimized for that outcome rather than for engineering completeness. When SEO and conversion-rate optimization conflict, SEO wins (the design best-practices global-rules.md makes this rule explicit).
A few brand facts are locked and worth knowing before you touch anything:
- Canonical domain:
whitetreedigital.com. While DNS is pending, the live site runs athttps://whitetreedigital.emanuelgold.workers.dev. - Contact email:
hello@whitetreedigital.com. - The shorter
whitetree.digitalwas never a real domain — never write or reintroduce it.
Optimize for leads, not completeness
This is a lead-gen site. Button copy must be action-led ("Book a strategy call", "Request a proposal") — never "Learn more" or "Submit". Voice is plainspoken, receipts over promises. The single source of truth for visual and voice decisions is website/Brand Guidelines.html.
Two independent packages
The repo holds two independent repos and packages with no shared Git history. Read the relevant child CLAUDE.md before touching either.
| Package | What it is | Deploys to | How it deploys |
|---|---|---|---|
website/ | The marketing site — Astro 6 + Tailwind v4 + React 19 islands | Cloudflare Workers | Git push to main (+ a content deploy hook) |
studio/ | Sanity v5 Studio — content authoring for the site | whitetreedigital.sanity.studio | npx sanity deploy from studio/ |
How they relate
The relationship is deliberately one-directional: the studio owns the content, the website reads it.
studio/is where content is edited — schemas, page-builder sections, and the documents authors fill in. Project ID8ftz0iuv, single datasetproduction.website/only ever reads from the Sanity dataset; it never writes back. (Content flows the other way — see the Sanity mental model.)- They share a mirror contract: each
*Section.astrocomponent in the website corresponds to one*Sectionobject schema in the studio. Schema changes instudio/that affect rendered fields require matching updates inwebsite/src/sanity/types.tsandwebsite/src/components/SectionRenderer.astro.
The website never writes to Sanity
website/ is read-only against the dataset. Never write to Sanity from website code — content authoring is the studio's job. Adding a brand-new page-builder section type is the one change that touches both packages; the adding-a-section page (and studio/CLAUDE.md) has the step-by-step checklist.
Schema vs content travel different paths
The core mental model that unlocks the two-package setup: schema and content live in different places and flow in opposite directions.
| Schema (the shape) | Content (the values) | |
|---|---|---|
| Lives in | studio/ TypeScript files → git | Sanity's hosted database (JSON docs) |
| Edited by | You, in code | The owner, in Studio's web UI |
| Reaches its target via | npx sanity deploy (to Studio) | the website's Sanity fetch (to the site) |
| Version-controlled | Yes (git) | No (Sanity history only) |
A schema change is a code deployment, not a live edit — the hosted Studio only picks it up when you run sanity deploy. Content edited in Studio never lands as a file on your machine; it lives only in Sanity's database and reaches the site at fetch time. This split is the subject of the Sanity mental model page.
The stack at a glance
The website is built on:
- Astro 6 + Tailwind v4 (via
@tailwindcss/vite) - React 19 via
@astrojs/react— installed for interactive islands, not the default for static content - Sanity v5 as the content source (the Studio is the separate
studio/package; the site only reads) - HubSpot Forms for lead capture, with the free website-audit tool as a lead magnet
- GA4 + HubSpot tracking, both via GTM (no direct script injection)
- Cloudflare Workers via
@astrojs/cloudflarewithoutput: 'server' - Node ≥22.12
The studio is Sanity v5 + React 19, with the Presentation Tool wired for visual editing.
Workers, not Pages — and SSR is required
The site deploys to Cloudflare Workers, not Pages. The @astrojs/cloudflare v13 adapter emits Workers-shaped output (dist/server/entry.mjs + dist/server/wrangler.json with dist/client/ for static assets), not CF Pages shape. output: 'server' is load-bearing: draft preview reads the perspective cookie at request time, which can't be done from prerendered HTML. See deployment for the full contract.
Where content meets code
Most pages are page-builder driven: a Sanity document carries a sections[] array of section objects, and SectionRenderer.astro dispatches each one to its matching *Section.astro component by _type. Adding a root-level page is therefore content-only — create and publish a page document with a unique slug and it resolves at request time. The page builder, content model, and routing pages cover this in depth.
A couple of cross-cutting footguns are worth flagging here because they bite across the whole codebase:
Stega breaks string equality in draft preview
In draft/preview mode the Sanity preview client encodes every string field with invisible Unicode tag characters (for click-to-edit). Those chars are invisible but not zero-length, so they silently break exact comparisons: variant === 'sidebar' fails. Any time a Sanity string drives JS logic (equality, switch, regex, an object/array index key), call stegaClean() from @sanity/client/stega first. Rendering plain strings is fine; only clean when the value participates in logic. The bug is invisible in production and only surfaces in draft preview.
Never delete a Sanity singleton, and never build while the dev server runs
Two operational landmines: (1) Never run npx sanity documents delete <singleton-id> to "reset" a singleton — Sanity tombstones the ID and initialValue won't recover it; use createOrReplace. (2) Don't run npm run build in website/ while npm run dev is up — it corrupts the dev server's Vite cache. npx astro check is safe alongside dev.
Two builds from one repo
The website ships as two Cloudflare Workers built from the same codebase, selected by the BUILD_TARGET env var at build time:
- Public (
whitetreedigital.com) —BUILD_TARGET=static, content pages prerendered to HTML. Sanity is not in the request path, so a Sanity outage can't take the public site down. Content goes live only on a manual deploy or code push. - Staging (
staging.whitetreedigital.com) —BUILD_TARGETunset → full SSR. This is the live-editing target that Studio's Presentation Tool iframes; it reads draft content per request and is kept private vianoindex+ an HMAC draft-cookie gate.
One environment variable decides which. The full rationale, the route-file seams that serve both builds, and the resilience story live on the two-build split page.
Publishing is instant in staging, manual to public
Publishing a document in Studio is instant in preview/staging, but reaches the public site only on the next manual deploy (Studio's "Deploy" tool fires a deploy hook) or a code push. This is intentional — iterative drafts don't each trigger a wasteful public rebuild.
Where this lives
| Topic | File |
|---|---|
| Monorepo intro + cross-package rules | CLAUDE.md (repo root) |
| Website guide — stack, layout, conventions | website/CLAUDE.md |
| Website at a glance + commands | website/README.md |
| Studio guide — schemas, page-builder, singletons | studio/CLAUDE.md |
| Studio at a glance + Presentation workflows | studio/README.md |
| Schema vs content mental model | Sanity Mental Model.md (repo root) |
| Two-build operations & maintenance | _TWO-BUILD-OPERATIONS.md (repo root) |
| Brand, voice, and visual rules | website/Brand Guidelines.html |
| Per-page-type design rules | website/_design-best-practices/ |
From here, the natural next steps are development setup, the monorepo architecture, and environment variables.