Sanity Page Builder
Set up the kit, edit pages in Studio, remove the collections you don't need and deploy
The Lexington Sanity Page Builder is a block-based Sanity CMS starter for Astro. Editors build every page in Sanity Studio from a library of blocks, with click to edit and live preview. Developers get typed GROQ, Tailwind CSS tokens and one folder per block. See it running on the live demo.
It is a pnpm monorepo:
apps/
web/ The Astro site: the base layout, the catch-all route, API routes
studio/ Sanity Studio: pages and settings, desk structure, Presentation, the seed script
packages/
blocks/ One folder per block (schema.ts, query.ts, Component.astro) and the registries
collections/ One folder per collection: its document, blocks, details page and layouts
fields/ Shared schema fields: link, figure (image with alt), block options, body
ui/ Tokens (colors.css, fonts.css), Text, Button, Wrapper, icons, shader
types/ Generated types (sanity.types.ts) and the BlockOf helperGet the code
- Get the Page Builder from its product page.
- In your Polar library, connect your GitHub account and accept the invite to the private repository.
- Clone it, or use it as a template for each new site.
You need Node 22.12 or newer, pnpm 11 (corepack enable sets it up) and a free account at sanity.io.
Setup
1. Install
pnpm install2. Create a Sanity project
Create a project at sanity.io/manage with a production dataset, and copy its project ID. Or from the terminal:
cd apps/studio
pnpm exec sanity login
pnpm exec sanity projects create "My site" --dataset production3. Create two tokens
In sanity.io/manage, open your project, then API, then Tokens, and add:
- a Viewer token: the site uses it to preview drafts in the Studio
- an Editor token: the site uses it to save form submissions
Both stay on the server. Never put them in a variable that starts with PUBLIC_.
4. Fill in the environment files
cp apps/web/.env.example apps/web/.env
cp apps/studio/.env.example apps/studio/.env| Variable | File | What it is |
|---|---|---|
SANITY_PROJECT_ID, SANITY_DATASET | web | Your project ID and production |
SANITY_API_READ_TOKEN | web | The Viewer token |
SANITY_API_WRITE_TOKEN | web | The Editor token |
SANITY_STUDIO_URL | web | Where the Studio runs, http://localhost:3333 locally |
RESEND_API_KEY, FORMS_NOTIFY_EMAIL, FORMS_FROM_EMAIL | web | Optional: also email every form submission through Resend |
SANITY_STUDIO_PROJECT_ID, SANITY_STUDIO_DATASET | studio | The same project ID and dataset |
SANITY_STUDIO_PREVIEW_URL | studio | Where the site runs, http://localhost:4330 locally |
5. Add the demo content (optional)
pnpm seedThis writes every demo page, collection entry and the Settings document to your dataset, with its images. It replaces documents with the same IDs and leaves everything else alone, so you can run it again to reset the demo.
To start empty instead: open the Studio, create a page, then open Settings and pick it as the home page.
6. Run it
pnpm devThe site runs on localhost:4330 and the Studio on localhost:3333. The Presentation tab in the Studio shows the site beside the editor: click any text on the page to open its field, and changes show as you type.
Scripts
| Command | What it does |
|---|---|
pnpm dev | Runs the site and the Studio |
pnpm build | Builds both |
pnpm seed | Writes the demo content to your dataset |
pnpm typegen | Regenerates packages/types/sanity.types.ts after a schema or query change |
pnpm strip <collection> | Removes collections you don’t need |
pnpm --filter @lexington/studio deploy | Deploys the Studio to <name>.sanity.studio |
Edit the site
- Pages: every page is a list of blocks. Add one with Add item: blocks are grouped by category and searchable. Each block has a Content tab and an Options tab (layout, background, anchor).
- Collections: posts, changelog releases, customers, case studies, services, jobs, events, locations, help topics, integrations, legal pages and team members each have their own detail page. Their index pages (
/blog,/careersand so on) are ordinary pages with a block that lists them. - Layouts: every collection has more than one detail layout. Pick the default in Settings, then Layouts, or override it on one entry.
- Settings: site name, logo, home page, navigation, footer, accent colours, the default share image and the support box on help topics.
- Inbox: what visitors send through the forms, filtered by form. Editors can read and delete submissions, but can’t edit them.
- Redirects: send an old path to a new one, permanent (301) or temporary (302). They go live within a minute.
- SEO: every page and entry has an SEO title and description. Pages also take a share image; posts and releases use their own image; everything else falls back to the default share image in Settings.
/sitemap.xmllists every published page.
Removing a collection
Each collection lives in its own folder in packages/collections: its document, the blocks that list it, its details page and layouts. To start a site with only the ones it needs:
pnpm strip events locations
pnpm seedstrip deletes the folders, takes them out of the lists in packages/collections and regenerates the types. The Studio desk, Settings, Presentation, the sitemap and the routes follow on their own. pnpm seed then leaves out their demo content and deletes what it wrote before: their entries, their index page, their block category page, and the links and cards that pointed at them. Restart pnpm dev after.
Some collections reference another: blog posts, events and locations name team members, and services link to case studies. strip refuses to remove team or case studies on their own while those remain, so remove them together or keep both.
Run pnpm strip with no names to list the collections.
Forms
Every form posts to apps/web/src/pages/api/forms.ts, which:
- drops bots without telling them: a hidden field people never fill, and a minimum of three seconds between loading the page and sending
- saves the submission as a
submissiondocument (the Studio’s Inbox), with any uploaded CV as a file - emails it to
FORMS_NOTIFY_EMAILwhen the three Resend variables are set - sends the visitor back to the same page with a thank you note under the form
Forms work without JavaScript. The form kinds live in packages/blocks/_shared/formStatus.ts; collections add their own (careers adds job applications, events adds registrations). The sign in and sign up blocks are layouts only: wire them to your own auth.
Re-skin it
- Colours:
packages/ui/styles/colors.css. Background, foreground, muted and muted foreground set the ground; accent, accent strong, highlight and info set the brand. The[data-tone="dark"]block flips the ground for dark blocks. Editors can also change the four brand colours in Settings, then Theme, without a deploy. - Fonts: put the font files in
apps/web/public/fonts, change the@font-facerules and the font tokens inpackages/ui/styles/fonts.css, and the preload list inapps/web/src/components/head/Fonts.astro. - Type scale and buttons:
packages/ui/elements/Text.astroandButton.astro./system/typography,/system/buttons,/system/colorsand/system/linksshow every variant.
Add a block
A block is a folder in packages/blocks with three files, and one line in each registry:
schema.ts: the fields editors see, withblockGroupsandblockOptionsFieldfrom@lexington/fieldsquery.ts: what the site fetches, ending inoptionsQuery, plus its type asBlockOf<"yourBlock">YourBlock.astro: the component, wrapped inBlockso it takes the block options
Then register it in packages/blocks/schemas.ts, queries.ts, components.ts and groups.ts (the category in the Add item menu), and run pnpm typegen. components.ts checks that every registered block has a component, so a missing one fails the type check. The repository README walks through a full example.
Deploy
The site
The site renders on demand, so it needs a host that runs server code. It ships with the Node adapter (@astrojs/node): start it with node apps/web/dist/server/entry.mjs after pnpm build:web.
For Vercel, Netlify or Cloudflare, swap the adapter:
cd apps/web
pnpm astro add vercel # or netlify, or cloudflareThen remove the node adapter lines in apps/web/astro.config.mjs, set site to your domain, and add every variable from apps/web/.env to your host before the build. On your host, set the root to the repo and the build command to pnpm build:web.
The Studio
pnpm --filter @lexington/studio deployThis hosts the Studio at <name>.sanity.studio. Then point the two apps at each other:
SANITY_STUDIO_URLon the site: the deployed Studio URLSANITY_STUDIO_PREVIEW_URLinapps/studio/.env: your live site, then deploy the Studio again
To host the Studio on your own domain instead, run pnpm build:studio, serve apps/studio/dist, and add that domain in sanity.io/manage under API, then CORS origins, with credentials allowed.
Work with AI agents
AGENTS.md sets out the conventions, .cursor/skills/add-block walks an agent through adding a block, and .cursor/skills/page-builder-design holds the design system (tokens, type, buttons, spacing, corners) so new blocks match the rest. If you re-skin the kit, update that skill too. .cursor/mcp.json connects the Sanity MCP server, so an agent can read your schema and content.