Any agent
Clone seba-seb/okapi-sanity into /tmp/okapi and follow its INSTALL.md to add Okapi to this project.
Sanity + a visual editor for Astro · open source
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.
Product
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:
Login, roles, drafts, history, images and CDN. We don't maintain any of it. The Studio lives at /studio on your site.
Our visual editor at /studio#/editor. You type directly on the page, drag blocks, style with your tokens and build components.
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.
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… | Administrator | Editor | Contributor |
|---|---|---|---|
| Edit text, rich text, links and images | Yes | Yes | Yes |
| Reorder blocks within the design | Yes | Yes | Yes |
| Insert, duplicate and delete blocks | Yes | Some blocks | No |
| Style panel (spacing, color, radius, breakpoints) | Yes | No | No |
| Create, edit and detach components | Yes | No | No |
| Use the AI chat (if the project has it on) | Yes | Yes | No |
| Publish | Yes | Yes | No |
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
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.
Clone seba-seb/okapi-sanity into /tmp/okapi and follow its INSTALL.md to add Okapi to this project.
Add Okapi to this project.
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.
Anything that needs your account. The agent gives you the exact command or steps:
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
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
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.
Map our colors, spacing and radii to Okapi's tokens, without renaming them, so the style panel offers our palette.
Make the editor pages use our header and footer instead of Okapi's.
Make /services come from the editor: build it in Sanity with Okapi blocks and remove the hardcoded page.
Blocks
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.
Delete the Okapi example blocks we don't use in this project.
Permissions and chat
Let clients add headings, paragraphs and images, but not change styles or delete sections.
Turn on the editor's AI chat with Anthropic, offer only Sonnet and Haiku, and tell me exactly what to set on Vercel.
Publish and maintain
Connect publishing in the editor to our Vercel deployment, and tell me exactly what I need to configure myself.
How do I invite Ana (ana@client.com) so she can edit text but not publish?
Bring the latest version of Okapi into this project: update src/core from the repo, tell me if any schema changed and run verify.
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.
With your agent
The split that keeps the work organized:
| Done in the editor | Asked of your agent |
|---|---|
| Text, links and images | New blocks, or making something that already exists editable |
| Adding, reordering and duplicating sections and blocks | How a block looks |
| A block's style, within the tokens | The token values (your design) |
| Building and reusing components | Routes, layout, header and footer of the editor pages |
| Publishing | Connecting publishing to deployment |
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.Product
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.
useEditStateuseDocumentOperationloadSite() + one query per page, no CDN[data-island] islands + Studio at /studioThe canvas lives on its own route (/edit-canvas), separate from the Studio:
postMessage.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
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.
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 what | JS (gzip) | When |
|---|---|---|
Page without js blocks | ≈1 KB | Always (just the script that looks for islands) |
| Page with islands: React DOM, blocks, Portable Text | 91 KB | If there's at least one island (slider, 3D logo) |
| three.js for the 3D Logo block | 140 KB | Only if the page has that block |
| Studio + Okapi Editor | ≈1,650 KB | Only for whoever opens /studio |
| Editor canvas | 144 KB | Only 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.
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.og:*, Twitter card, sitemap.xml, robots.txt and JSON-LD Organization on the home page.pageHref() and resolveLink(). Internal links are references to pages, not hand-typed paths. Adding languages later means changing those two functions.Product
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).
| Shortcut | Action |
|---|---|
| ⌘ Z · ⌘ ⇧ Z | Undo · redo (typing is grouped) |
| ⌘ D | Duplicate the selection |
| ⌘ C · ⌘ V | Copy · paste blocks (also between pages) |
| ⌫ | Delete the selection |
| ⌥ ↑ · ⌥ ↓ | Move up · down one position |
| Esc | Select the parent block |
| ⌘ B · ⌘ I · ⌘ K | Bold · italic · link, while editing rich text |
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.
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).
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.
Same-origin postMessage messages, typed in core/editor/protocol.ts.
| Direction | Message | Purpose |
|---|---|---|
| Editor → canvas | render | Document, routes and components to draw |
| Editor → canvas | select | Highlight the selection, with optional scrolling |
| Canvas → editor | ready | The iframe has loaded |
| Canvas → editor | select | Click on a block (with add for multi-select) |
| Canvas → editor | text | Edited text (string or Portable Text) |
| Canvas → editor | move | Drag on the canvas: before or after another block |
| Canvas → editor | drop | Image file dropped on a block |
| Canvas → editor | key | Shortcuts pressed while the canvas has focus |
Product
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.
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.
| Property | Values |
|---|---|
| Margin, padding | none xs sm md lg xl 2xl 3xl |
| Background, text color | bg surface surface-2 fg muted brand brand-contrast border |
| Radius | none sm md lg main pill |
| Max width | prose 65ch · narrow 40rem · page · full |
| Text alignment, hide | left center right · hide per breakpoint |
| Breakpoints | tablet ≤ 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; }
}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
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.
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.
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
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.
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 role | Sanity plan | What Sanity allows | In Okapi (default) |
|---|---|---|---|
administrator | All | Everything, including project settings | Everything |
editor | Growth and above | Read, write and publish | Text, images, reorder, some blocks, publish, chat |
contributor | Growth and above | Write drafts, no publishing | Text, images, reorder |
viewer | All (free seat) | Read only | Look only |
developer and others | Growth and above | Depends on the role | Look only, unless the policy names it |
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.
| Permission | Allows |
|---|---|
text | Edit texts, rich text and links |
images | Replace images |
style | The style panel (tokens only) |
settings | Block settings: variants, sizes, layout |
reorder | Move blocks up and down |
structure | Add, duplicate and delete blocks (limited by blocks) |
components | Make, detach and insert library components |
publish | Publish |
chat | Use the AI chat |
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.
/api/editor-chat.AI_MODELS limits the list; the first is the default.features.chat: false in src/okapi.config.json the site stays fully static.Product
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.
| Block | Contains | JS | On the canvas |
|---|---|---|---|
| Section | Any block | No | Theme, spacing, background (drop an image) |
Component componentRef | An instance from the library (if components are on) | Depends on content | Text and image overrides |
| Eyebrow | — | No | In-place text |
| Heading | — | No | In-place rich text |
| Paragraph | — | No | In-place rich text |
| Button Group | Button | No | — |
| Button | — | No | In-place label |
| Image | — | No | Drop an image |
| Looping Video | — | No | — |
| Content | Eyebrow, Heading, Paragraph, Button Group | No | — |
| Card | — | No | In-place title and body, drop an image |
| Grid | Card, Content, Image, 3D Logo | No | — |
| Box | Any block | No | — |
| Slider | Card, Image | Yes | Optional at install |
| 3D Logo | — | Yes · three.js loaded separately | Optional at install |
Every block also has the style panel. "JS" indicates whether the block makes its section hydrate on the public site.
Integration
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.
The Okapi repo is already a complete Astro project: clone it, remove its .git and run npm install.
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.
| Copied | What 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.ts | page, component, settings, navigation |
src/theme/ | Tokens (names the editor uses) and the blocks' CSS. Only editor pages load it |
src/layouts/Base.astro | Layout for editor pages (or OkapiBase.astro if there's already a Base) |
src/pages/[slug].astro, edit-canvas.astro | Editor pages and the canvas. index.astro only if the home page comes from the editor |
sanity.config.ts, sanity.cli.ts | The Studio |
scripts/verify.mjs, check-editor-schema.ts | Checks |
src/okapi.config.json, src/okapi.policy.ts, scripts/okapi-setup.mjs | What the project uses, who can do what, and the script that applies the install answers |
src/chrome/, public/placeholder.svg | Header and footer for editor pages (replaceable) and the default image for new blocks |
AGENTS.md | Rules 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.
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.
| Flag | Question | If no |
|---|---|---|
--slider | Slider / carousel? | The block and embla-carousel-react are deleted |
--logo3d | 3D logo? (~140 KB, only on pages that use it) | The block and three are deleted |
--components | Reusable components? | features.components: false: no library and no instances |
--chat | AI chat in the editor? | features.chat: false: no adapter, no server function, fully static |
--roles | Who edits? administrator · administrator+editor · administrator+editor+contributor · keep (Sanity's role names) | Writes okapi.policy.ts with those roles |
--demo | Example 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.
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
What your code has to respect for the editor to work. Everything else is up to you.
| Rule | Why |
|---|---|
Everything editable is a block registered in src/blocks/meta.ts, schemas.ts and components.tsx | The 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 root | The canvas draws the document it receives and selects by data-sanity-path |
js in meta.ts if the block runs anything in the browser | Otherwise 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 do | The 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 library | If 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.ts | Core 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 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.
{...editable(…)} on their root. They never fetch: the canvas draws them with the document it receives.Foo.tsx and foo.ts in the same folder.Working with it
src/blocks/<type>/schema.ts with defineType and an icon from @sanity/icons, plus <Type>.tsx, which receives { block, path, ctx }.
An entry in meta.ts: title, slot if it contains other blocks, margin, and js if it runs anything in the browser.
Register it in schemas.ts and components.tsx. The compiler reports an error until you do.
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
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.
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"].
| Variable | Purpose |
|---|---|
PUBLIC_SANITY_PROJECT_ID | The Sanity project |
PUBLIC_SANITY_DATASET | Usually production |
PUBLIC_SANITY_API_VERSION | API version date, for example 2024-10-01 |
PUBLIC_SITE_URL | Origin 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_TOKEN | Only if the dataset is private: a Viewer token, read at build time |
ANTHROPIC_API_KEY · OPENAI_API_KEY · OPENROUTER_API_KEY | Only with the chat on: the project's key per provider, never PUBLIC_. Give it a spend limit at the provider |
AI_MODELS | Optional: 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
npm run verifyRun it before every handoff and after any change to core. It checks the things that break silently:
routes.check).check-editor-schema).ops.check).stylesheet.check).overrides.check).chat.check).richtext.check).npm run verify -- https://your-domain), it also checks that /, /studio and /edit-canvas respond.Flows that need a login can't be automated without credentials. Test them by hand, on a throwaway page, before handing off a site:
npm run build, and the change is on the static page.okapi.policy.ts allows shows up; the chat refuses the rest.Working with it
Each of these has come up before and has a known fix.
| Symptom | Cause | Fix |
|---|---|---|
| "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 login | The tab is requesting chunks from a previous server or deploy. | staleChunkReload reloads once on its own. If it persists: ⌘⇧R. |
| Blank Studio in Brave only | Shields 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 dev | The layout doesn't include <Islands />. | Add it at the end of <body>. |
| I published and the site didn't change | The site is static. | Check the webhook to the deploy hook, or run a build. |
| The ✦ Chat tab says it isn't available | No 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
Why it's built this way. Before changing any of these, read the reasoning.
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.
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.
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.
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.
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.
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.
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.
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.
Problem. No current project needs it.
Decision. Every URL goes through pageHref() and resolveLink(). Adding languages happens in one place.
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.
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.
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
The milestones, grouped. The details are in the repo's commits.
core, blocks and theme, with the typed block registry.link type, Settings and Navigation, header and footer, full SEO, npm run verify and CLAUDE.md./lab with its three stress-test sites./preview and visual-editing are removed. The site becomes 100% static.INSTALL.md and AGENTS.md for any agent. This documentation.okapi.policy.ts, with View as.seba-seb/okapi-sanity, MIT licence, brand and site in English and Spanish.npm run okapi:setup keeps only those and writes okapi.config.json.patches/ folder are gone.With your agent
Tell your agent exactly what you see. Each of these has happened before and has a known fix.
| You see | Most likely | Tell 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 blank | The 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 available | The 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 change | The 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 now | Okapi's CSS is loading outside its own pages. | "Since we added Okapi, /<route> looks different" |
With your agent
Ask your agent to "run verify". Then, logged in with your own user, on a throwaway page:
okapi.policy.ts allows shows up; the chat refuses the rest.With your agent
/studio. The editor lives inside it.CLAUDE.md.okapi-astro fetches INSTALL.md and follows it.src/core, the editor's code. It's the same in every project and gets fixed in the Okapi repo.Context
Ordered by impact. None of these blocks current use.
| What | Why | Size |
|---|---|---|
| Library gallery | Browse components with a live thumbnail and drag them onto the canvas. Today they're a list of names. | Small |
| Components of any size | Save a card or a button as a component and insert it inside sections, grids or boxes. | Medium |
| Insert as a copy or linked | Choose when inserting from the library. | Small |
| Custom Studio UI | Replace Sanity's toolbar and frame through the official API (studio.components) so the editor is the whole app. | Medium |
| Per-block JS | So a section with only a slider doesn't load every block. Cuts the 91 KB on pages with little JS. | Medium |
| Private npm package | Install and update with npm instead of copying src/core. Once there are 3 or more sites in production. | Medium |
| Languages | When a client asks for it. The groundwork is in pageHref and resolveLink. | Medium |
| 3D gizmo on the canvas | Rotate the 3D Logo directly on the canvas. Postponed. | Paused |