OkapiDocs
Home GitHub ↗

Sanity + a visual editor for Astro · open source

Okapi

Okapi adds two things to an Astro project, new or existing: Sanity as the database, and a visual editor of its own where your team and your clients build and edit pages on top of the real design.

Your coding agent installs it, whichever one you use, by following a guide in the repo. This view explains how to ask for it and how to work with it afterwards.

  • Astro 7.3 · static
  • Sanity 6.19+ · current
  • React 19
  • Tailwind 4 on tokens
≈1 KBof JS on a page without blocks that need it
91 KBof JS (gzip) on a page with a slider or a 3D logo
0 KBof editor code on the public site, checked on every build
2paths: a new project or an existing Astro project
You're reading the guide for working with your agent.
You're reading the technical documentation.

Product

What it is

It isn't a site template. Okapi is the content and editing layer you add to an Astro project. Your project stays yours: your pages, your design and your deployment. Okapi adds:

Sanity as the database

Login, roles, drafts, history, images and CDN. We don't maintain any of it. The Studio lives at /studio on your site.

Okapi Editor

Our visual editor at /studio#/editor. You type directly on the page, drag blocks, style with your tokens and build components.

Pages built in the editor

The routes you choose are built in the editor and served as static HTML. Only sections with a slider or a 3D logo load JavaScript.

Roles: the same names as in Sanity

Okapi uses Sanity's role names as they are. You give each person a role when you invite them to the Sanity project, and src/okapi.policy.ts decides what that role sees in the editor. This is the default; the agency changes it per project. An Administrator can preview any role with View as.

Can…AdministratorEditorContributor
Edit text, rich text, links and imagesYesYesYes
Reorder blocks within the designYesYesYes
Insert, duplicate and delete blocksYesSome blocksNo
Style panel (spacing, color, radius, breakpoints)YesNoNo
Create, edit and detach componentsYesNoNo
Use the AI chat (if the project has it on)YesYesNo
PublishYesYesNo

Usually the agency is Administrator, a client who publishes is Editor, and someone who only leaves drafts is Contributor. Viewer (and any other role) only looks. Editor and Contributor exist on Sanity's Growth plan and above; see Roles and invitations.

With your agent

Add it to your project

The installation is written up in the repo's INSTALL.md, designed so any coding agent can follow it: Claude Code, Codex, Cursor or another. Open your agent in the project folder and ask.

Any agent

Clone seba-seb/okapi-sanity into /tmp/okapi and follow its INSTALL.md to add Okapi to this project.
The agentDetects whether the project is new or existing, asks you whatever is missing, copies what it needs without overwriting your files, configures Sanity and runs verify.
You checkThat your pages are unchanged, that /studio opens and that the editor shows the page.

Claude Code, with the skill installed

Add Okapi to this project.
The agentThe okapi-astro skill brings the same INSTALL.md and follows it.
You checkThe same things. Install the skill once, from the repo, with cp -R skills/okapi-astro ~/.claude/skills/.

New or existing project

  • Empty folder: the agent starts from the Okapi repo, which is already an Astro project with example blocks that you replace later.
  • Existing project: the agent first analyzes it and tells you what fits and what's risky, before installing anything. It stops if the project uses Tailwind 3, Preact or Solid, already has Sanity, or runs Astro older than 7.3. If it goes ahead, your pages, layout and configuration stay as they are, and editor pages are served on routes that don't exist yet, or wherever you tell it. Your existing pages stay in code: to edit one of their sections in the editor, it has to become a block.

What it will ask you

  • Which Sanity organization the project goes in, or the project ID if you've already created one.
  • For an existing project: which routes come from the editor, and whether the home page comes from the editor or stays yours.
  • Which modules you want: slider, 3D logo, reusable components and the AI chat. What you don't pick isn't installed.
  • Who will edit, using Sanity's role names: only Administrator (your team), plus Editor (a client who publishes), plus Contributor (someone who only leaves drafts).
  • Whether to load example content to see the editor working right away.

Before changing anything, it shows you the plan (which blocks go, which dependencies are removed, which roles stay) and waits for your OK. The chat and components can be switched on or off later in src/okapi.config.json.

What you do yourself

Anything that needs your account. The agent gives you the exact command or steps:

  • Log in to Sanity and, if you want, import the example page.
  • Invite the client.
  • On your host: environment variables (including the AI key, if you use the chat), the domain in CORS and the publish webhook.
  • The logged-in editor test (Before you hand off).
Never paste tokens or passwords into the chat

The guide is written so that no agent needs them. If an agent asks for one, don't give it: tell it to follow INSTALL.md.

With your agent

What to ask for next

Once Okapi is installed, these are the most common requests. They work with any agent: the rules it follows are in the AGENTS.md left in your project.

Connect your project

Make an existing component editable

Turn the home page Hero into an Okapi block, with the title, subtitle, button and image editable in the editor. The design must stay exactly the same.
The agentCreates the block from your adapted component, registers it, makes its text editable on the page and runs verify.
You checkIn the editor: + Add → Hero, type on the page and compare it with the original.

Your design in the style panel

Map our colors, spacing and radii to Okapi's tokens, without renaming them, so the style panel offers our palette.
The agentFills in tokens.css with your design's values. The names stay, because the editor uses them.
You checkOpen a block's style panel and look for your colors and spacing.

Your header and footer

Make the editor pages use our header and footer instead of Okapi's.
The agentChanges the editor pages' layout to use your components.
You checkAn editor page and one of your own pages show the same header.

Move a page to the editor

Make /services come from the editor: build it in Sanity with Okapi blocks and remove the hardcoded page.
The agentBuilds the page from existing blocks (or proposes new ones) and removes the old route. If it has the Sanity MCP, it creates the document; if not, it tells you how.
You checkThat /services looks the same and can be edited.

Blocks

A new block

Create a Testimonials block with a photo, quote, name and job title. A 3-column grid on desktop and a single column on mobile. The quote is edited directly on the page.
The agentSchema, component, registration, initial content and editable text on the canvas. Runs verify.
You check+ Add → Testimonials, then type the quote on the page.

Remove the example blocks

Delete the Okapi example blocks we don't use in this project.
The agentDeletes the folders and registry entries and runs verify. Fewer blocks with JS means lighter pages.
You checkThat they no longer appear in + Add.

Permissions and chat

Decide who can do what

Let clients add headings, paragraphs and images, but not change styles or delete sections.
The agentEdits src/okapi.policy.ts for the client's role and runs verify.
You checkIn the editor, View as that role: the style panel and those actions are gone.

Turn on the AI chat

Turn on the editor's AI chat with Anthropic, offer only Sonnet and Haiku, and tell me exactly what to set on Vercel.
The agentSets features.chat to true and gives you the variables for the host (ANTHROPIC_API_KEY, AI_MODELS).
You checkPaste the key on the host, with a spend limit at the provider, and redeploy. The ✦ Chat tab appears.

Publish and maintain

Publish = deploy

Connect publishing in the editor to our Vercel deployment, and tell me exactly what I need to configure myself.
The agentWalks you through the deploy hook, the Sanity webhook and the environment variables, step by step.
You checkPublish a change and see it on the site a few minutes later.

Invite the client

How do I invite Ana (ana@client.com) so she can edit text but not publish?
The agentGives you the command with the contributor role and explains what Ana will see.
You checkRun it and have Ana accept the invitation.

Update Okapi

Bring the latest version of Okapi into this project: update src/core from the repo, tell me if any schema changed and run verify.
The agentReplaces src/core with the new version, checks for schema changes and runs verify.
You checkThat the editor and the pages still work.

An editor bug

In the editor, dragging a card inside the grid moves it to the end. This is an Okapi issue: tell me what to change in the repo.
The agentFixes it in the Okapi repo (not just in your project), with a check so it doesn't come back.
You checkTry the case in the editor.

With your agent

How to ask

The split that keeps the work organized:

Done in the editorAsked of your agent
Text, links and imagesNew blocks, or making something that already exists editable
Adding, reordering and duplicating sections and blocksHow a block looks
A block's style, within the tokensThe token values (your design)
Building and reusing componentsRoutes, layout, header and footer of the editor pages
PublishingConnecting publishing to deployment
  • Token names don't change. Their values do. The style panel relies on those names.
  • The editor only sees registered blocks. For something to be editable, it has to be a block.
  • Okapi doesn't animate. If your project already uses GSAP, Motion or CSS animations, it keeps them: animation lives in your code, not in the editor.
  • Ask it to verify. "Run verify" before calling anything done.
  • Publishing is going live. With the webhook set up, every publish rebuilds the site within minutes. The agent never publishes without your explicit yes.
  • The chat can only do what your role can. Its changes are drafts and undo in one step. It never publishes.
  • Okapi gets fixed in its own repo. src/core is the same in every project: if something in the editor breaks, the fix goes to the repo and is then pulled into each project.
  • Sanity stays up to date. Okapi follows Sanity's current versions; after an upgrade, ask your agent to run verify and open the editor.

Product

Architecture

The core idea: the same PageBuilder component draws the page in the editor and on the site. What you see while editing is the real site, with its styles.

Why an iframe

The canvas lives on its own route (/edit-canvas), separate from the Studio:

  • Isolated CSS: the site's CSS doesn't mix with the Studio's.
  • Real breakpoints: desktop, tablet and mobile change the iframe's width.
  • No exposed drafts: the route is public but has no data of its own. It only draws what an already authenticated editor sends it, over same-origin postMessage.

Block paths

Every block carries data-sanity-path on its root, holding its GROQ path inside the document, for example:

sections[_key=="a1f3"].content[_key=="9c2e"].items[_key=="77d0"]

That path ties together the click on the canvas, the selected layer, the properties panel and the write to Sanity. Structural operations (core/editor/ops.ts) are pure functions over those paths, validated against the block registry. The undo history is a stack of document snapshots.

Product

The public site

Pages are static (output: "static"). If the AI chat is on and the host runs functions (Vercel, Netlify, Cloudflare Pages), Astro adds one server function, /api/editor-chat; everything else is still prerendered. With the chat off there is no adapter at all. At build time, each page requests its document from Sanity, and loadSite() fetches settings, navigation, the route map and the component library. Build reads bypass Sanity's CDN on purpose, and a production build stops if Sanity can't be reached or the env is missing, so a failed fetch never deploys an empty site.

HTML first, JavaScript only where needed

PageBuilder renders on the server. Every top-level section that contains a block with js in the registry is wrapped as an island:

<!-- display: contents → doesn't affect the layout -->
<div data-island='{"section":{…},"ctx":{…},"components":{…}}'>
  <section class="ok-section" data-ok-theme="dark">…already-rendered HTML…</section>
</div>

Base.astro includes a minimal script. If the page has at least one island, it imports core/render/hydrate.tsx, which runs hydrateRoot on each island with the same props. If there are none, the page doesn't load React.

Who downloads whatJS (gzip)When
Page without js blocks≈1 KBAlways (just the script that looks for islands)
Page with islands: React DOM, blocks, Portable Text91 KBIf there's at least one island (slider, 3D logo)
three.js for the 3D Logo block140 KBOnly if the page has that block
Studio + Okapi Editor≈1,650 KBOnly for whoever opens /studio
Editor canvas144 KBOnly inside the editor

npm run verify checks this on every build: no public HTML references editor or Studio code, and the home page doesn't exceed 250 KB of JS.

SEO included

  • Titles and description: every page has metaTitle, metaDescription, ogImage and noindex. When they're empty, they fall back to the values in Settings, and if those are empty too, to the defaults in code.
  • Generated tags: absolute canonical, og:*, Twitter card, sitemap.xml, robots.txt and JSON-LD Organization on the home page.
  • URLs: all of them come from pageHref() and resolveLink(). Internal links are references to pages, not hand-typed paths. Adding languages later means changing those two functions.

Product

Okapi Editor

It's the Studio's first tab. It has three columns: pages and layers, canvas and properties. Along the top are undo and redo, + Add, the device selector and Publish. The right column has two tabs, Properties and ✦ Chat. Both side columns resize by dragging their edge (double-click resets).

What you can do

  • In-place text. Double-click headings, paragraphs, eyebrows, buttons, and card titles and bodies. Rich text has a floating toolbar with bold, italic, link and unlink.
  • Layers. A tree with drag and drop (before, after or inside), insert, duplicate, delete, move, and copy and paste between pages.
  • Dragging on the canvas. The selected block shows a ⠿ handle. A blue line shows where it will land, and the move is validated with the same rules as the layers.
  • Multi-select. ⇧ or ⌘ + click on the canvas or in the layers. Delete, duplicate, copy and move apply to all of them.
  • Images. Drag a file onto an image, card or section background, or upload it from the panel.
  • Properties generated from the schema. Every field of every block has its own control; verify fails if any field is left without one.
  • ✦ Chat. Ask for changes in your own words and attach up to 3 images. Only what your role allows; changes are drafts that undo in one step. See Permissions and AI chat.

Shortcuts

ShortcutAction
⌘ Z · ⌘ ⇧ ZUndo · redo (typing is grouped)
⌘ DDuplicate the selection
⌘ C · ⌘ VCopy · paste blocks (also between pages)
⌫Delete the selection
⌥ ↑ · ⌥ ↓Move up · down one position
EscSelect the parent block
⌘ B · ⌘ I · ⌘ KBold · italic · link, while editing rich text

Saving and publishing

Everything you do is saved automatically as a draft. Nobody sees it until you press Publish. After publishing, the site updates on the next build: a couple of minutes if the project has the webhook set up.

How it saves

The editor uses the Studio's own document store, with no additional SDK. Changes apply locally right away and are saved as a draft. Publish publishes the document. Since the site is static, publishing triggers a rebuild (see Hosting and publishing).

Text editing without caret jumps

While you type, the canvas holds back the renders coming from the editor. When you finish, it restores the exact DOM React expects, and only then applies the new value. Rich text is converted between Portable Text and editable HTML in core/editor/richtext.ts, which keeps the targets of existing links by their key.

Editor ⇄ canvas protocol

Same-origin postMessage messages, typed in core/editor/protocol.ts.

DirectionMessagePurpose
Editor → canvasrenderDocument, routes and components to draw
Editor → canvasselectHighlight the selection, with optional scrolling
Canvas → editorreadyThe iframe has loaded
Canvas → editorselectClick on a block (with add for multi-select)
Canvas → editortextEdited text (string or Portable Text)
Canvas → editormoveDrag on the canvas: before or after another block
Canvas → editordropImage file dropped on a block
Canvas → editorkeyShortcuts pressed while the canvas has focus

Product

Styles and tokens

All of the design comes from src/theme/tokens.css, the first file you change in every project. It holds the colors per theme (light, dark, brand), the radii, the type scale, spacing and page width. Tailwind 4 exposes them as utilities (bg-ok-surface text-ok-fg gap-ok-md rounded-ok-main), and since colors read the section's data-ok-theme, every block adapts to the theme on its own. Tailwind's default palette is removed on purpose: a new color is added as a token. Everything Okapi defines carries a prefix (--ok-* variables, ok-* classes, ok- utilities), so it never clashes with the project's own Tailwind, shadcn or DaisyUI.

Style panel

Every block has a design field that the editor shows as the style panel. It offers tokens only, no free-form CSS, and each value can be set for all screens, tablet and mobile.

PropertyValues
Margin, paddingnone xs sm md lg xl 2xl 3xl
Background, text colorbg surface surface-2 fg muted brand brand-contrast border
Radiusnone sm md lg main pill
Max widthprose 65ch · narrow 40rem · page · full
Text alignment, hideleft center right · hide per breakpoint
Breakpointstablet ≤ 1023px · mobile ≤ 767px (desktop-first)

core/style/stylesheet.ts turns those values into a single stylesheet per page, with one selector per block. Panel values use !important because the panel has the final say over that block:

[data-b="9c2e"][data-b] { padding-top: var(--ok-space-lg) !important; }
@media (max-width: 767px) {
  [data-b="9c2e"][data-b] { padding-top: var(--ok-space-sm) !important; }
}

Box

The free-form container: it stacks, lays out in a row or builds a grid, with token gaps, alignment and wrapping. It can be nested. On tablet, grids drop to 2 columns and on mobile to 1; rows stack on mobile.

Product

Components

A component is a component document that holds sections. A page uses it through a componentRef block (an instance). If you edit the component, all of its instances change.

In the editor

  • Make component: saves the selected section to the library and replaces it with an instance.
  • + Add: lists the library's components so you can insert them as instances.
  • Edit component: opens the original component for editing.
  • Detach: turns the instance into a standalone, editable copy.
  • The canvas selects an instance as a single block.

Per-instance overrides

If you double-click a text or drop an image inside an instance, the change is saved on that instance only, in its overrides list. The original is untouched. Each override targets "<blockKey>.<field>", is applied at render time (core/render/overrides.ts), and the panel lists them with a Reset button.

Current limitation

Components are whole sections and only live at the top level of a page. They can't be nested or inserted inside another section. See What's next.

Product

Permissions and AI chat

Each project decides who can do what in src/okapi.policy.ts: every Sanity role gets a list of permissions and, optionally, the block types it may add. The editor and the chat follow it. The chat can never do more than the person using it.

Roles and invitations

The role is set in Sanity, not in Okapi. Invite people in sanity.io/manage → your project → Members → Invite and pick the role there (you can change it later from the same list), or from a terminal: npx sanity users invite ana@client.com --role editor.

Sanity roleSanity planWhat Sanity allowsIn Okapi (default)
administratorAllEverything, including project settingsEverything
editorGrowth and aboveRead, write and publishText, images, reorder, some blocks, publish, chat
contributorGrowth and aboveWrite drafts, no publishingText, images, reorder
viewerAll (free seat)Read onlyLook only
developer and othersGrowth and aboveDepends on the roleLook only, unless the policy names it
On Sanity's free plan

Only administrator and viewer exist, so a client who edits has to be an administrator and sees everything. To separate the agency from the client you need the Growth plan.

PermissionAllows
textEdit texts, rich text and links
imagesReplace images
styleThe style panel (tokens only)
settingsBlock settings: variants, sizes, layout
reorderMove blocks up and down
structureAdd, duplicate and delete blocks (limited by blocks)
componentsMake, detach and insert library components
publishPublish
chatUse the AI chat
The policy shapes the editor; it isn't a lock

The real boundary is the Sanity role: what the API lets that person write. Someone technical can write anything their role allows without going through the editor. Give clients the narrowest Sanity role that works.

How the chat works

  • The editor sends the request, with the person's Sanity session, to the site's own function /api/editor-chat.
  • The function checks the session with Sanity, works out the person's permissions, reads the page from Sanity and asks the model with the project's key, stored in the host. The key never reaches the browser.
  • The model only proposes changes. The editor checks each one against the permissions, the block registry and the schema, and applies the valid ones as one undo step, saved as a draft. The chat never publishes.
  • Images: attach up to 3 (📎, paste or drop). The original is uploaded to Sanity; the model sees a smaller copy.
  • Model picker: every provider with a key on the host appears (Anthropic, OpenAI, OpenRouter). AI_MODELS limits the list; the first is the default.

Cost and privacy

  • One key per project, with its own spend limit at the provider. Every request has a token cap and a per-person rate limit; the drafts-only role has no chat by default.
  • The page being edited and any attached images are sent to the chosen provider. Say so to the client.
  • The chat is optional: with features.chat: false in src/okapi.config.json the site stays fully static.

Product

Blocks

Okapi ships with these example blocks. At install, npm run okapi:setup removes the optional ones you don't want (Slider, 3D Logo); each project then adds its own. The single registry is src/blocks/meta.ts. It defines which types exist, what they can contain, which ones need JS and what the editor offers in each place. schemas.ts and components.tsx are typed against the registry: if something is missing, the compiler flags it.

BlockContainsJSOn the canvas
SectionAny blockNoTheme, spacing, background (drop an image)
Component componentRefAn instance from the library (if components are on)Depends on contentText and image overrides
Eyebrow—NoIn-place text
Heading—NoIn-place rich text
Paragraph—NoIn-place rich text
Button GroupButtonNo—
Button—NoIn-place label
Image—NoDrop an image
Looping Video—No—
ContentEyebrow, Heading, Paragraph, Button GroupNo—
Card—NoIn-place title and body, drop an image
GridCard, Content, Image, 3D LogoNo—
BoxAny blockNo—
SliderCard, ImageYesOptional at install
3D Logo—Yes · three.js loaded separatelyOptional at install

Every block also has the style panel. "JS" indicates whether the block makes its section hydrate on the public site.

Integration

Installation

The full guide is INSTALL.md, at the root of the repo, written for any agent to follow. There are two paths, and both end with the same shared steps: Sanity, .env, first run, roles, production and verify.

Path A: new project

The Okapi repo is already a complete Astro project: clone it, remove its .git and run npm install.

Path B: existing project

First the agent analyzes the project and shows a report with ✅, ⚠️ and 🛑. 🛑 means it doesn't install: Astro older than 7.3, Preact or Solid (Okapi sets the project's JSX to React), Tailwind 3 (Okapi needs Tailwind 4 and a project can't have both), or Sanity already set up. ⚠️ means it goes on only if you accept it: Vue or Svelte, several languages, dynamic routes at the root. Okapi's [slug] outranks a blog's or Starlight's [...slug], so the project's slugs go in reservedSlugs, or editor pages move to /p/<slug> with pagesBase. A project without Tailwind is fine: Okapi's Tailwind has no preflight and only loads in its own layouts. The project's trailingSlash stays as it is: Okapi's routes, links, canonicals and sitemap follow it.

Then pieces are copied into the project without overwriting anything that already exists. If a route collides, the agent stops and asks.

CopiedWhat it is
src/core/The product: editor, rendering, styles, Sanity. Not modified per project
src/blocks/The registry and the example blocks. Delete blocks or add your own
src/documents/, src/schema.tspage, component, settings, navigation
src/theme/Tokens (names the editor uses) and the blocks' CSS. Only editor pages load it
src/layouts/Base.astroLayout for editor pages (or OkapiBase.astro if there's already a Base)
src/pages/[slug].astro, edit-canvas.astroEditor pages and the canvas. index.astro only if the home page comes from the editor
sanity.config.ts, sanity.cli.tsThe Studio
scripts/verify.mjs, check-editor-schema.tsChecks
src/okapi.config.json, src/okapi.policy.ts, scripts/okapi-setup.mjsWhat the project uses, who can do what, and the script that applies the install answers
src/chrome/, public/placeholder.svgHeader and footer for editor pages (replaceable) and the default image for new blocks
AGENTS.mdRules for agents. If the project already has one, it's added as a section

Tested on Astro's official blog template: build and verify pass, the blog is untouched and src/core is unchanged.

Choosing what to install

Before installing, the agent asks which modules the person wants and runs npm run okapi:setup with the answers: first with --dry-run to show the plan, then with --yes. If an answer is missing, the script does nothing and says which one. Run in a terminal without flags, it asks the questions itself. It refuses to run inside the Okapi repo.

FlagQuestionIf no
--sliderSlider / carousel?The block and embla-carousel-react are deleted
--logo3d3D logo? (~140 KB, only on pages that use it)The block and three are deleted
--componentsReusable components?features.components: false: no library and no instances
--chatAI chat in the editor?features.chat: false: no adapter, no server function, fully static
--rolesWho edits? administrator · administrator+editor · administrator+editor+contributor · keep (Sanity's role names)Writes okapi.policy.ts with those roles
--demoExample content?No seed file; the first page is created in the editor

Blocks are deleted, so the site ships less code. Chat and components are flags that core reads (core/config.ts), so core stays the same in every project and they can be switched later. Checks pass with any combination: tested with everything off, the page JS dropped and a Vercel build came out fully static.

Configuration that gets merged

  • package.json: the Sanity, React 19 and Tailwind 4 dependencies (plus Embla and three if the slider and the 3D logo stay, and the host adapters if the chat is on), engines: node >=22.12 and the okapi:setup script. Sanity at its current versions: sanity ^6.19, @sanity/client 8, @sanity/ui 4, @sanity/icons 5. If the project has newer ones, they stay; older ones are raised within the same major.
  • astro.config: nothing is pasted. It imports okapi(), okapiSanity(), okapiAdapter() and okapiSite() from src/core/astro/okapi.ts and adds them next to react() and the Tailwind plugin. okapi() brings the chat route, the project's final trailingSlash, Sanity's image CDN and the optimizeDeps list, merged with the project's own. The project's output and site are kept.
  • tsconfig: React JSX and the @/* → ./src/* alias.

Integration

The contract with your project

What your code has to respect for the editor to work. Everything else is up to you.

RuleWhy
Everything editable is a block registered in src/blocks/meta.ts, schemas.ts and components.tsxThe registry defines what exists, what can contain what and what the editor offers
A block receives { block, path, ctx }, never fetches, and puts {...editable(…)} on its rootThe canvas draws the document it receives and selects by data-sanity-path
js in meta.ts if the block runs anything in the browserOtherwise the site serves it as HTML and it never hydrates
Every layout that renders <PageBuilder islands /> includes <Islands />It hydrates the sections that need JS (slider, 3D logo), and in dev it installs the React preamble
The names in tokens.css don't change; the values doThe style panel offers those names
Links go through resolveLink() and pageHref()A single source of URLs: links, sitemap, canonical, future languages
Okapi ships no animation libraryIf the project already animates (GSAP, Motion, CSS), it keeps doing so, under its own licence. The editor has no animation controls
Project choices live in src/okapi.config.json and src/okapi.policy.tsCore reads them, so core never needs per-project edits
Icons from their subpath (@sanity/icons/Cube) and moved UI pieces from theirs (@sanity/ui/tooltip)That's how current Sanity packages export them

A project doesn't touch src/core: editable text and image drop are declared per block in meta.ts. The only exception is pageHref() if editor pages are mounted under a prefix.

Working with it

The code

The rule the product rests on: src/core is the same in every project. A fix in core is made in the Okapi repo and then pulled into each project. What's specific to each project lives in blocks, theme and its layouts. Once there are 3 or more projects in production, core becomes a private package and fixes arrive with npm update.

src/core/ the product · the same in every project editor/ EditorTool, CanvasApp, FieldControls, ops, paths, protocol, richtext render/ PageBuilder, BlockRenderer, instances, overrides, Islands.astro + hydrate style/ panel tokens, per-page stylesheet, design field schema sanity/ client + loadQuery, site loader, queries, editable(), env studio/ custom Studio inputs (sliders, enums, responsive) lib/ routes (pageHref/resolveLink), clean, staleChunkReload, types src/okapi.config.json what the project uses: features.chat, features.components src/okapi.policy.ts who can do what, per Sanity role (editor + chat) src/blocks/ one folder per block · meta.ts = the registry src/theme/ tokens.css (the first thing you change) + globals.css src/chrome/ the project's header and footer src/documents/ page, component, settings, navigation src/pages/ [slug] = editor pages · /edit-canvas = canvas scripts/ verify, check-editor-schema, seeds skills/ okapi-astro skill (Claude Code): follows INSTALL.md docs/ this documentation INSTALL.md installation into a new or existing project, for any agent AGENTS.md rules for working on the code (imported by CLAUDE.md)

Rules most often broken

  • Blocks receive everything as props and carry {...editable(…)} on their root. They never fetch: the canvas draws them with the document it receives.
  • Style with Tailwind utilities built on the tokens. No literal colors.
  • Design assets (logos, textures) are code; they don't go in the CMS.
  • macOS is case-insensitive: never put Foo.tsx and foo.ts in the same folder.

Working with it

Adding a block

  1. Schema and component

    src/blocks/<type>/schema.ts with defineType and an icon from @sanity/icons, plus <Type>.tsx, which receives { block, path, ctx }.

  2. Registry

    An entry in meta.ts: title, slot if it contains other blocks, margin, and js if it runs anything in the browser.

  3. Maps

    Register it in schemas.ts and components.tsx. The compiler reports an error until you do.

  4. Editor (optional)

    Content on insert in defaults.ts, editable text (text) and image drop (imageField) in the block's meta.ts entry. If a role has a blocks list, add the type there too.

// src/blocks/meta.ts
quote: { title: "Quote", margin: true },
ticker: { title: "Ticker", js: true },
gallery: { title: "Gallery", js: (b) => b.layout === "carousel" },
// src/blocks/quote/Quote.tsx
export function Quote({ block, path, ctx }: BlockProps<QuoteData>) {
  return (
    <blockquote className="text-ok-h-lg text-ok-fg"
      {...editable(ctx.documentId, ctx.documentType, path, block._type)}>
      {block.text}
    </blockquote>
  );
}

If the component already exists in your project, the path is the same: the component reads everything from block instead of props or hardcoded text, and if it was an .astro file it becomes .tsx.

To remove a block, delete its folder, its entry in meta.ts, the two map entries and any accepts that names it. The compiler takes you to each one.

Working with it

Hosting and publishing

Any host works. Build with npm run build, output dist, Node 22.12+ and npm. With the AI chat on, use Vercel or Netlify (built in) or Cloudflare Pages (npm i @astrojs/cloudflare wrangler once): Okapi picks the adapter from where the build runs and adds the chat's single function. On any other host the site works and the chat shows as unavailable.

Publishing doesn't change the site until the next build

Without the webhook, the change is published in Sanity but the site keeps showing the previous version. Create the webhook in sanity.io/manage → API → Webhooks, with the filter _type in ["page","settings","navigation","component"].

VariablePurpose
PUBLIC_SANITY_PROJECT_IDThe Sanity project
PUBLIC_SANITY_DATASETUsually production
PUBLIC_SANITY_API_VERSIONAPI version date, for example 2024-10-01
PUBLIC_SITE_URLOrigin for canonical, og:url and the sitemap. On Vercel, Netlify and Cloudflare it falls back to the deployment URL if missing. An empty value counts as unset.
SANITY_API_READ_TOKENOnly if the dataset is private: a Viewer token, read at build time
ANTHROPIC_API_KEY · OPENAI_API_KEY · OPENROUTER_API_KEYOnly with the chat on: the project's key per provider, never PUBLIC_. Give it a spend limit at the provider
AI_MODELSOptional: the models the chat offers, e.g. anthropic:claude-sonnet-5-5, anthropic:claude-haiku-5-5. The first is the default

Every domain you open /studio from has to be in the project's CORS origins, with credentials allowed. Without that, the Studio shows "Connect this studio to your project".

The deploy hook URL is a secret: whoever has it can trigger builds. Keep it only in Sanity's webhook and the host.

Working with it

Quality and testing

npm run verify

Run it before every handoff and after any change to core. It checks the things that break silently:

  • The installed Sanity version has its patch.
  • Links and routes resolve correctly (routes.check).
  • Every field of every block has a control in the panel (check-editor-schema).
  • Insert, move, delete and duplicate keep the document valid, and island detection is correct (ops.check).
  • Tokens produce the expected CSS, with no selector injection (stylesheet.check).
  • Overrides apply to the right block and never touch the original (overrides.check).
  • Each role's permissions gate every change the chat proposes, and provider requests have the right shape (chat.check).
  • Rich text round-trips to editable HTML without breaking out of attributes (richtext.check).
  • The build passes, the public HTML contains no editor or Studio code, and the home page stays at or under 250 KB of JS.
  • The home page has real content, an absolute canonical, a sitemap and robots.txt.
  • Given a URL (npm run verify -- https://your-domain), it also checks that /, /studio and /edit-canvas respond.

Logged-in smoke test

Flows that need a login can't be automated without credentials. Test them by hand, on a throwaway page, before handing off a site:

  1. Type into a heading on the page, reload, and check that the text is still there.
  2. Bold, italic and a link in a paragraph. Insert, duplicate, delete and drag a layer. Undo and redo.
  3. Drop an image on an Image block: it uploads and shows up.
  4. Make component on a section. Edit text on an instance: only that one changes. Reset. Edit component: all of them change.
  5. Publish, then npm run build, and the change is on the static page.
  6. Log in as a client-role user: they don't see styles or structure actions.
  7. With View as each role, only what okapi.policy.ts allows shows up; the chat refuses the rest.
  8. ✦ Chat: "make the selected heading shorter" is applied, and Undo reverts it.

Working with it

Known issues

Each of these has come up before and has a known fix.

SymptomCauseFix
"Connect this studio to your project"The origin isn't in CORS. Each port counts as a different origin.Add the exact origin, with credentials. Locally, use :4321.
Blank Studio or canvas + 504 (Outdated Optimize Dep)Vite re-optimized dependencies while the tab was open.Stop the server, rm -rf node_modules/.vite, npm run dev, ⌘⇧R.
Blank Studio right after loginThe tab is requesting chunks from a previous server or deploy.staleChunkReload reloads once on its own. If it persists: ⌘⇧R.
Blank Studio in Brave onlyShields and routing.Solved with hash routing and basePath: "/".
The home page shows the welcome screen.env or the content is missing.Fill in .env and import the seed.
The build stops with [loadQuery] …Sanity didn't respond or the env is missing. It stops on purpose so an empty site isn't deployed.Check PUBLIC_SANITY_* on the host and Sanity's status, then rebuild.
"can't detect preamble" in devThe layout doesn't include <Islands />.Add it at the end of <body>.
I published and the site didn't changeThe site is static.Check the webhook to the deploy hook, or run a build.
The ✦ Chat tab says it isn't availableNo AI key on the host, a host that can't run functions, or features.chat is off.Set a key (ANTHROPIC_API_KEY…), use Vercel/Netlify/Cloudflare and redeploy.

Context

Decisions

Why it's built this way. Before changing any of these, read the reasoning.

Our own editor instead of Sanity Presentation

Problem. Presentation was slow for editing: you had to click the overlay, find the field in a form and wait for the round trip. You couldn't type on the page, and it depended on 7 patches.

Decision. Replace only the editing UI, using public Studio APIs. Presentation, /preview, draft mode and visual-editing were removed.

Sanity as the database

Problem. Login, roles, drafts, history, real-time updates and images are months of infrastructure work.

Decision. Don't build any of that: Sanity handles it, and the Studio stays available for settings, menus and collections.

An iframe canvas with no data of its own

Problem. The site's CSS and the Studio's can't mix, and a canvas route with data would expose drafts.

Decision. A same-origin iframe that only draws what the authenticated editor sends it. It also gives real breakpoints.

A static site with per-section islands

Problem. Hydrating the whole page cost 138 KB of JS even when almost nothing moved.

Decision. Without Presentation, there's no need to hydrate everything: plain HTML, and React only in sections with js.

Sanity up to date, no patches

Problem. Okapi used to pin sanity@6.3.0 because of patches written for Presentation. Projects on newer Sanity couldn't install it cleanly.

Decision. Presentation is gone, so the patches went too. Okapi follows Sanity's current versions; typecheck and verify catch breaking changes.

Token-only styling

Problem. Free-form CSS and absolute positioning amount to building an entire page builder, and they break brand consistency.

Decision. The panel only offers tokens. New blocks are written in code, and the editor composes and styles with them.

Copied, not a package yet

Problem. A private package from day one adds versioning before you know what changes per project.

Decision. Install by copying, following a guide for agents, with src/core left intact. Turn it into a package once there are 3 or more sites in production.

Installation for any agent

Problem. The team doesn't use a single agent, and an installation tied to one goes stale in the others.

Decision. An INSTALL.md and an AGENTS.md in plain Markdown. The Claude Code skill just fetches them and follows them.

No i18n yet, but ready for it

Problem. No current project needs it.

Decision. Every URL goes through pageHref() and resolveLink(). Adding languages happens in one place.

No animation library

Problem. Shipping GSAP and animated blocks tied every project to one library and its licence terms for visual builders, and added weight.

Decision. Okapi doesn't animate. Projects that already animate keep their own tools. Page JS with islands went from 139 KB to 91 KB.

The AI chat runs on the site's server

Problem. A key in the browser can be read by any script in the Studio, and clients would see it.

Decision. One function per site, /api/editor-chat, with the project's key in the host. It verifies the Sanity session and the role before asking the model.

Ask before installing

Problem. Installing everything by default left projects with modules and dependencies nobody asked for.

Decision. The agent asks first and okapi:setup applies the answers the same way every time.

Context

History

The milestones, grouped. The details are in the repo's commits.

  1. Astro port and foundation

    • Next.js page builder ported to Astro 7. Split into core, blocks and theme, with the typed block registry.
    • The link type, Settings and Navigation, header and footer, full SEO, npm run verify and CLAUDE.md.
    • Tailwind 4 on the tokens. First animations with SplitText.
  2. Motion blocks and test bench

    • Horizontal Scroll, Stacked Cards, Marquee, Scroll Text, Big Type and Parallax Media, plus /lab with its three stress-test sites.
    • Speed improvements to the dev environment.
  3. Okapi Editor

    • Phase 0: a Studio tool with a live canvas and a panel generated from the schema.
    • Phase 1: in-place text, layers with drag and drop, structural operations, undo and images.
    • Presentation, Vision, /preview and visual-editing are removed. The site becomes 100% static.
    • Phase 2: style panel with tokens and breakpoints, the Box block and components with instances.
    • Phase 3: in-place rich text, per-instance overrides, multi-select, dragging on the canvas and ▶ Play.
  4. Weight and robustness

    • The public site imports nothing from the editor.
    • Per-section islands: pages without motion drop from 138 KB to ≈1 KB of JS.
    • Install and verify fail if Sanity drifts from its patch.
    • Installable in existing Astro projects, with INSTALL.md and AGENTS.md for any agent. This documentation.
  5. Permissions and AI chat

    • Per-role permissions in okapi.policy.ts, with View as.
    • ✦ Chat on the site's server function, with the project's key, a model picker and image attachments.
    • Resizable side columns in the editor.
  6. Okapi, no GSAP, audit

    • Remarkable is renamed Okapi; repo seba-seb/okapi-sanity, MIT licence, brand and site in English and Spanish.
    • GSAP, the animated blocks and ▶ Play are removed. Page JS with islands: 139 KB → 91 KB.
    • Audit: escaped JSON-LD and canvas HTML, builds stop if Sanity is unreachable, chat cost caps, publishing = going live.
  7. Install asks first

    • The agent asks which modules to install; npm run okapi:setup keeps only those and writes okapi.config.json.
    • Sanity up to date (6.19, client 8, ui 4, icons 5): the pin to 6.3.0 and the patches/ folder are gone.

With your agent

If something breaks

Tell your agent exactly what you see. Each of these has happened before and has a known fix.

You seeMost likelyTell your agent
"Connect this studio to your project"You opened it on a different port. The local one is :4321."I'm getting Connect this studio on localhost:<port>"
The Studio goes blankThe tab went stale after the server restarted.First ⌘⇧R. If it persists: "The Studio is blank, check the console"
The home page shows "Your visual page builder is ready"The configuration or the content is missing."The home page shows the welcome screen"
The ✦ Chat tab says it isn't availableThe project has no AI key on the host, or the chat is off."The chat says it isn't available"
I published and the site didn't changeThe webhook is missing, or the build hasn't finished yet."I published and the site didn't change"
One of my own pages looks different nowOkapi's CSS is loading outside its own pages."Since we added Okapi, /<route> looks different"

With your agent

Before you hand off

Ask your agent to "run verify". Then, logged in with your own user, on a throwaway page:

  1. Type into a heading on the page, reload, and check that the text is still there.
  2. Bold, italic and a link in a paragraph. Insert, duplicate, delete and drag a block. Undo and redo.
  3. Drop an image on an Image block: it uploads and shows up.
  4. Create a component, change the text on one instance (only that one changes) and edit the original (all of them change).
  5. Publish and see the change on the site after the build.
  6. Log in as the client: they don't see styles or structure actions.
  7. With View as each role, only what okapi.policy.ts allows shows up; the chat refuses the rest.
  8. ✦ Chat: "make the selected heading shorter" is applied, and Undo reverts it.
  9. Settings filled in: site name, description and social sharing image.
  10. The domain is in CORS and the publish webhook works.

With your agent

Glossary

Block
A piece of the page: a heading, paragraph, image, card, slider…
Section
A horizontal band of the page that holds blocks. It has a theme (light, dark or brand), spacing and a background.
Box
A free-form container for laying out blocks in a column, a row or a grid.
Component
A section saved to the library so it can be reused across pages.
Instance
Each use of a component on a page. If you change the component, every instance changes.
Override
A text or image changed on a single instance, without touching the component.
Token
A named design value: a color, a spacing step, a radius. Changing it changes the whole site.
Draft
What you've edited but haven't published yet. Only the team sees it.
Publish
Put content live. With the webhook, the site rebuilds within minutes.
Build
When the site is generated from the published content. It takes a few seconds, plus install time.
Webhook
The notification Sanity sends on publish so your host runs a build.
Studio
Sanity's admin panel, at /studio. The editor lives inside it.
Canvas
The view of the page inside the editor, where you click and type.
verify
The project's automated check: if it passes, the important parts work.
INSTALL.md
The guide any agent follows to add Okapi to a project.
AGENTS.md
The rules the agent reads before working on the project. Claude Code reads them through CLAUDE.md.
Skill
A Claude Code shortcut: okapi-astro fetches INSTALL.md and follows it.
Core
src/core, the editor's code. It's the same in every project and gets fixed in the Okapi repo.
okapi:setup
The install script: keeps only the modules you chose.
okapi.config.json
What the project uses: the chat and components, on or off.
Role
Administrator, Editor, Contributor or Viewer: set in Sanity when you invite someone. Okapi uses the same names.
okapi.policy.ts
Who can do what in the editor and the chat, per Sanity role.
✦ Chat
The editor's AI assistant. It proposes changes; you keep or undo them.

Context

What's next

Ordered by impact. None of these blocks current use.

WhatWhySize
Library galleryBrowse components with a live thumbnail and drag them onto the canvas. Today they're a list of names.Small
Components of any sizeSave a card or a button as a component and insert it inside sections, grids or boxes.Medium
Insert as a copy or linkedChoose when inserting from the library.Small
Custom Studio UIReplace Sanity's toolbar and frame through the official API (studio.components) so the editor is the whole app.Medium
Per-block JSSo a section with only a slider doesn't load every block. Cuts the 91 KB on pages with little JS.Medium
Private npm packageInstall and update with npm instead of copying src/core. Once there are 3 or more sites in production.Medium
LanguagesWhen a client asks for it. The groundwork is in pageHref and resolveLink.Medium
3D gizmo on the canvasRotate the 3D Logo directly on the canvas. Postponed.Paused
Okapi · repo seba-seb/okapi-sanity · installation in INSTALL.md · rules in AGENTS.md · updated October 19, 2026