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 at https://whitetreedigital.emanuelgold.workers.dev.
  • Contact email: hello@whitetreedigital.com.
  • The shorter whitetree.digital was 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.

PackageWhat it isDeploys toHow it deploys
website/The marketing site — Astro 6 + Tailwind v4 + React 19 islandsCloudflare WorkersGit push to main (+ a content deploy hook)
studio/Sanity v5 Studio — content authoring for the sitewhitetreedigital.sanity.studionpx 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 ID 8ftz0iuv, single dataset production.
  • 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.astro component in the website corresponds to one *Section object schema in the studio. Schema changes in studio/ that affect rendered fields require matching updates in website/src/sanity/types.ts and website/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 instudio/ TypeScript files → gitSanity's hosted database (JSON docs)
Edited byYou, in codeThe owner, in Studio's web UI
Reaches its target vianpx sanity deploy (to Studio)the website's Sanity fetch (to the site)
Version-controlledYes (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/cloudflare with output: '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_TARGET unset → full SSR. This is the live-editing target that Studio's Presentation Tool iframes; it reads draft content per request and is kept private via noindex + 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

TopicFile
Monorepo intro + cross-package rulesCLAUDE.md (repo root)
Website guide — stack, layout, conventionswebsite/CLAUDE.md
Website at a glance + commandswebsite/README.md
Studio guide — schemas, page-builder, singletonsstudio/CLAUDE.md
Studio at a glance + Presentation workflowsstudio/README.md
Schema vs content mental modelSanity Mental Model.md (repo root)
Two-build operations & maintenance_TWO-BUILD-OPERATIONS.md (repo root)
Brand, voice, and visual ruleswebsite/Brand Guidelines.html
Per-page-type design ruleswebsite/_design-best-practices/

From here, the natural next steps are development setup, the monorepo architecture, and environment variables.

Previous
Getting started