@basementstudio/sanity-plugin-csv-export
v0.1.0
Published
Portable Sanity Studio plugin for exporting documents as CSV — pick types and fields, preview, download.
Downloads
92
Readme
@basementstudio/sanity-plugin-csv-export
A Sanity Studio v5 tool that lets you select document types and fields (via searchable dropdowns), preview results, and download them as a clean RFC-4180 CSV or XML file — all from within the Studio.
Installation
bun add @basementstudio/sanity-plugin-csv-exportpnpm add @basementstudio/sanity-plugin-csv-exportnpm install @basementstudio/sanity-plugin-csv-exportUsage
Add csvExport() to your plugins array in sanity.config.ts (or .js):
import {defineConfig} from 'sanity'
import {csvExport} from '@basementstudio/sanity-plugin-csv-export'
export default defineConfig({
// ...
plugins: [csvExport()],
})A CSV Export entry (with a download icon) will appear in the Studio top navigation.
With options
plugins: [
csvExport({
includedTypes: ['post', 'author', 'company'],
defaultDelimiter: ';',
filenamePrefix: 'acme-export',
}),
],Options
All options are optional.
| Option | Type | Default | Description |
|---|---|---|---|
| includedTypes | string[] \| (ctx: {currentUser}) => string[] \| undefined | all document types | Allowlist of document type names to show in the type picker. The function form receives the current user, so you can mirror role-based Studio structure (return undefined to allow all types). Namespaced system documents (sanity.imageAsset, media.tag, translation.metadata, …) are hidden by default — an explicit allowlist fully controls visibility, so you can deliberately re-add one. |
| excludedTypes | string[] | [] | Document type names to hide. Applied after includedTypes. Useful for hiding internal/system types. |
| serializers | Partial<Record<FieldKind, Serializer>> | — | Override the built-in cell serializer for one or more field kinds. |
| defaultFormat | 'csv' \| 'xml' | 'csv' | Output format pre-selected in the options bar. Users can switch it per export. |
| defaultDelimiter | ',' \| ';' \| '\t' | ',' | Default CSV delimiter pre-selected in the options bar (CSV only). Users can still change it per export. |
| filenamePrefix | string | workspace title (slugified) | Prefix for the downloaded file, used as-is (only path-illegal characters are stripped). Result: <prefix>-<types>-<timestamp>.csv — e.g. accel-company-2026-07-27-1432.csv. When every type is selected the type list collapses to all; more than 3 types are capped with a +N count. |
Matching the Structure navigation
To offer the same document types a user sees in the Structure sidebar, pass a function to includedTypes using the same role logic your structure resolver uses:
csvExport({
includedTypes: ({currentUser}) => {
if (isAdmin(currentUser)) return undefined // all types
const roles = currentUser?.roles?.map((r) => r.name) ?? []
for (const role of roles) {
const allowed = ROLE_ALLOWED_TYPES[role]
if (allowed) return [...allowed]
}
return undefined
},
excludedTypes: ['metadata'], // internal types hidden from the sidebar
})All documents / All fields
Both pickers start with an All documents / All fields toggle. Neither is on by default — the tool starts with no types selected and only _id pre-checked.
Union field list & name collisions
With multiple types selected, the field list is the union across all selected types; fields absent on a given type render as empty cells. If two types declare the same field name with a different shape (e.g. status is a string on one type and a reference on another), each conflicting type gets its own disambiguated column — labeled e.g. Status (Ticket) — so no type's data is exported through another type's projection.
Serializer override example
Note:
slug,reference,image, andfilevalues are flattened inside the GROQ projection (slug.current,ref->title,asset->url), so a serializer override for those kinds receives the already-resolved string, not the raw object — it acts as a string formatter. Overrides forportableText,array,object, andgeopointreceive the raw value.
Override how Portable Text fields are serialized — for example to strip all markup and join paragraphs with a pipe character:
import {csvExport} from '@basementstudio/sanity-plugin-csv-export'
import type {Serializer} from '@basementstudio/sanity-plugin-csv-export'
interface Block {
children?: {text?: unknown}[]
}
const myPortableTextSerializer: Serializer = (value) => {
if (!Array.isArray(value)) return ''
return value
.map((block) => {
const children = (block as Block)?.children ?? []
return children.map((child) => (child?.text == null ? '' : String(child.text))).join('')
})
.filter(Boolean)
.join(' | ')
}
export default defineConfig({
plugins: [
csvExport({
serializers: {
portableText: myPortableTextSerializer,
},
}),
],
})Field types
The plugin resolves complex Sanity field types to flat strings before writing CSV cells:
| Field kind | Serialization |
|---|---|
| primitive | String(value) — covers string, number, boolean, date |
| slug | The .current value (resolved via GROQ projection) |
| reference | Resolved referenced document title (via GROQ -> dereference); multi-target references coalesce across each target's title field |
| image | Asset CDN URL (via GROQ asset->url dereference) |
| file | Asset CDN URL |
| portableText | Plain text extracted from block children, paragraphs joined with newline |
| array | Primitive items joined with ; — complex items serialized as JSON |
| object | JSON.stringify(value) |
| geopoint | lat,lng coordinate pair |
Output formats
Pick the format in the options bar:
- CSV — RFC-4180, UTF-8 BOM, configurable delimiter (
,/;/ tab), optional header row. - XML — one
<document>per row; each cell is<field name="key" label="Label">value</field>with the column identifier in an attribute (so any field key is represented safely) and all values XML-escaped.
The same field selection and serialization (references → title, images → URL, Portable Text → plain text, …) feed both formats.
CSV injection protection
Cell values that would execute as spreadsheet formulas (starting with =, @, or with +/- when not a plain number) are prefixed with a single quote (') so Excel and Google Sheets treat them as literal text (CWE-1236).
Drafts / perspective
The tool offers an Include drafts toggle. When enabled the GROQ query runs with perspective: 'drafts', which returns the draft version of documents where one exists, falling back to the published version. When disabled, only published documents are returned.
How it works
Field discovery is schema-driven: the plugin reads the live Sanity schema via useSchema() and walks each selected document type's field tree to produce a flat list of FieldDescriptor objects (key, path, label, kind). Those descriptors are compiled into a GROQ projection that fetches only the chosen fields, using -> dereferencing for references and images. Large datasets are fetched in pages using keyset pagination (ordering by _id), inspired by Sanity's Exporting content as CSV guide. The collected rows are encoded client-side as RFC-4180 CSV with a UTF-8 BOM () so the file opens correctly in Excel and Numbers without manual encoding steps.
Requirements
- Sanity Studio v5+
- React 19+
License
MIT © Basement Studio
