violetchen39 downloadsPublish and pull Obsidian documents to/from your Sanity studio (bidirectional sync).

Sanity is a plugin for Obsidian that lets you publish and pull documents between your Obsidian vault and your Sanity Studio — a bidirectional sync, not tied to any specific blog or website.
This plugin is not on the Community Plugins marketplace. Install it manually or via BRAT.
violet27chen/sanity-obsidian-plugin
From your vault's root directory:
cd .obsidian/plugins && git clone https://github.com/violet27chen/sanity-obsidian-plugin.git
Restart Obsidian, then enable the plugin in Settings → Installed Plugins.
The plugin is completely schema-agnostic — it does not hard-code any field names. Every field name you enter in the settings must come from your own Sanity schema. Before filling the settings, know these four identifiers:
| Concept | What it is | Where to find it |
|---|---|---|
| Project ID | A short ID like 3hwpvo77 that identifies your Sanity project. |
sanity.io/manage → your project → the ID is shown in the URL and project header. |
| Dataset | A named container inside the project (usually production or development). |
sanity.io/manage → project → Datasets. |
| Document type | The type name you defined in your Studio schema (e.g. post, author, product, article). The plugin syncs one type at a time. |
Your Sanity Studio → Schema / Structure, or the _type field of a document. |
| Schema field | The individual field names inside that type, e.g. title, body, slug, heroImage, or any custom field you added. |
Your Studio schema definition, or open a document and look at its fields. |
💡 Rule of thumb: anything you type into the plugin settings (type name, title field, body field, GROQ paths in sync rows) must exactly match a name that exists in your Sanity schema. If a field name is wrong, sync silently skips it or the publish fails.
A quick mental model:
Open Settings → Sanity. Every field is explained below.
| Setting | Default | What to put |
|---|---|---|
| Sanity API token | empty | A Sanity API token with Editor (or Admin) role. A Viewer/read-only token will fail at publish with 403 insufficientPermissions (update). Generate one at sanity.io → project → API → Tokens. |
| Project ID | empty | Your Sanity project ID (e.g. 3hwpvo77). See the table above. |
| Dataset name | production |
The dataset to sync with (e.g. production, development). |
| Type name | post |
The document type to sync. This is your own schema type name — it can be post, but also author, product, article, note, or anything you defined. The plugin syncs all published documents of this type. |
| Title field | empty | The schema field that holds the title, e.g. title for a blog, name for an author, label for a product. Leave blank to skip syncing the title (filename is then used as title). |
| Body field | body |
The schema field that holds the document body as raw Markdown, e.g. body, bio, content, description. |
| Filename field | empty | Which Sanity field generates the Obsidian filename / slug. GROQ paths allowed, e.g. slug.current. Left blank → falls back to Title field → slug.current → sanity_id. |
| Content divider | empty | A marker string; anything below it in the note is NOT published. |
| Pull folder | empty | Folder where pulled notes are saved. Blank → vault root. |
| Custom API base URL | empty | Reverse-proxy URL for when api.sanity.io is unreachable (e.g. mainland China). All query/mutate/upload calls go through it. Blank → default host. |
| Additional fields to sync | empty | Extra Sanity fields ↔ frontmatter mappings (see below). Empty by default — add the fields you actually need. |
| Announcement text / link / type | empty / info |
Site announcement; used by the Publish announcement to Sanity command. Leave text empty to show nothing. |
This is the most flexible setting. It maps arbitrary Sanity fields into each note's frontmatter (and back on publish). It is empty by default — nothing is synced except the title and body until you add rows.
Sanity field : frontmatter key.slug.current, heroImage, social.github.[[ ]] are uploaded).categories[], tags[]) is written back as a YAML list — just write the field name, no special syntax.slug.current are expanded into nested objects on write (slug: { current: "..." }).image object (e.g. heroImage, avatar, cover) is auto-converted: on pull → a cdn.sanity.io URL in frontmatter; on publish → back to an asset reference (or uploaded if it is a local path).💡 Cover image vs body images Cover / thumbnail / avatar fields live here as image-object fields (e.g.
heroImage:heroImage,avatar:avatar). Body images inside the note (![[image]]or) are uploaded automatically on publish and do not need a row here.
Blog example (left → right):
slug.current → slug
description → description
publishedAt → published
categories → categories
series → series
heroImage → heroImage
Non-blog example — imagine a Sanity author type with fields name, bio, avatar (image), social (object with github), slug:
avatar → avatar
social.github → github
slug.current → slug
With that, the settings would be: Type name = author, Title field = name, Body field = bio, Filename field = slug.current, and the three rows above. The note's frontmatter would end up with avatar, github, and slug keys — note that github is a custom name you chose for social.github.
Delete all rows (or leave them blank) to sync only the title and body.
By default the plugin talks to https://<projectId>.api.sanity.io. In some networks (e.g. mainland China) that host is unreachable while the Sanity CDN (cdn.sanity.io) stays up. Enter a reverse-proxy URL — e.g. https://sanity-api.your-domain.com — and all API calls (query, mutate, upload) route through it. The path after the host is preserved, so nothing else changes. Leave blank to use the default host.
EdgeOne is a good fit here because its edge nodes are densely distributed across mainland China and the Asia–Pacific — clients in China are served from a nearby node, so latency drops and the Sanity API stays reachable where api.sanity.io is slow or blocked. If you run the proxy on Tencent EdgeOne's edge-acceleration platform, configure these three fields:
| Field | Value |
|---|---|
| Acceleration domain | Your own subdomain, e.g. sanity-api.your-domain.com — this is the URL you paste into Custom API base URL. |
| Origin domain | <projectId>.api.sanity.io (e.g. 3hwpvo77.api.sanity.io). Forward the path 1:1; do not rewrite it. |
| Origin request headers | Host: <projectId>.api.sanity.io and Authorization: Bearer <SANITY_TOKEN>. Sanity requires the Host header or it rejects the request; the Authorization header authenticates on the client's behalf. |
⚠️ The
Authorizationheader carries a real Sanity token. Only enable this on a proxy you control, and prefer a token scoped to your project.
Once the proxy is live, paste its URL into Settings → Sanity → Custom API base URL — every query, mutate, and upload call then routes through the edge proxy.
| Frontmatter key | Filled by | Meaning / how to use |
|---|---|---|
sanity_id |
plugin (auto) | The Sanity _id of the document. The round-trip key — publishing the note later updates the same Sanity document. Do not set it by hand. If it points to drafts.x, publish lands on the matching published doc x and deletes the draft. |
sanity_draft |
plugin (auto) | true/false — marks whether the source was a draft. Set by Pull; the publish logic does not consume it. |
Title field (whatever you configured, e.g. title / name) |
you / plugin | The title. On publish the plugin prefers this field; if absent it falls back to the filename. |
Body field (whatever you configured, e.g. body / bio) |
plugin | The document body (raw Markdown). |
Your sync-field keys (e.g. categories, slug, heroImage, github) |
you / plugin | Mapped via Additional fields to sync. Written back to Sanity on publish, filled in on pull. The key name is the one you put on the right of each sync row. |
Image fields (e.g. heroImage, avatar) |
you | Local image path ([[ ]] allowed) or a cdn.sanity.io URL. Uploaded / reference-restored automatically. |
Body images (
![[img]]/) are uploaded automatically on publish and replaced with CDN URLs — no frontmatter key needed.
Open the command palette (Ctrl/Cmd + P) and search Sanity:
sanity_id is written to the note. Publishes as a published document and removes any matching draft.siteSettings singleton.heroImage, categories, or your custom keys).![[image]] or .sanity_id into the frontmatter.sanity_id, sanity_draft, the title, and all configured sync fields.403 insufficientPermissions (update) at publish.drafts.*); it only brings published documents into Obsidian.Issues and pull requests are welcome.
This plugin is a fork of drewlyton/sanity-obsidian-plugin. Many thanks to @drewlyton for the original work this plugin is built upon.