@basementstudio/sanity-media-library
v0.2.0
Published
Folder-based media library tool and image asset source for Sanity Studio, with optional per-workspace scoping.
Downloads
238
Readme
@basementstudio/sanity-media-library
A folder-based media library for Sanity Studio: a Media tool for browsing, organizing and cleaning up image assets, plus a matching image asset source so editors pick from the same library on every image field.
It works on a plain Studio out of the box, and can additionally scope assets per workspace — several sites sharing one dataset, each seeing only its own images.
Features
- File-explorer UI: nested folders, grid/list views, sorting, search
- Folders nest to any depth, with a breadcrumb trail to navigate back up
- Drag and drop assets into folders (single or multi-select), including onto breadcrumb ancestors to move them up
- Upload straight into the current folder
- Asset dialog: rename, alt text, folder, dimensions/size/MIME info
- "Used by" tab listing the published documents referencing an asset
- Bulk delete and one-click cleanup of unreferenced assets — anything still in use is detected and kept
- Optional per-workspace scoping, with a control to reassign an asset to other scopes
- Image asset source that browses the same library from any image field, with the same grid/list views as the Media tool
Install
npm install @basementstudio/sanity-media-library
# or
bun add @basementstudio/sanity-media-libraryPeer dependencies: sanity v5, @sanity/ui v3, @sanity/icons v3, react and
react-dom v19.
Usage
// sanity.config.ts
import { defineConfig } from "sanity"
import { structureTool } from "sanity/structure"
import { mediaLibrary } from "@basementstudio/sanity-media-library"
export default defineConfig({
name: "default",
projectId: "…",
dataset: "production",
plugins: [structureTool(), mediaLibrary()],
})That registers a Media tool at /studio/media, a mediaFolder document type,
and an image asset source that replaces the built-in ones.
Scoped to a workspace
Scoping partitions one dataset between several Studio workspaces. Each workspace
passes its own scope.value; the tool and the asset source then only ever show
assets stamped with it.
const SITES = [
{ name: "business", title: "Business" },
{ name: "money", title: "Money" },
]
export default defineConfig(
SITES.map((site) => ({
name: site.name,
title: site.title,
basePath: `/studio/${site.name}`,
projectId: "…",
dataset: "production",
plugins: [
structureTool(),
mediaLibrary({
scope: {
value: site.name,
// Assets predating scoping carry no `sites` field — treat them as
// belonging to this scope.
fallback: "business",
// Shown in the asset dialog so an editor can share an asset.
options: SITES,
},
}),
],
}))
)An asset can belong to several scopes at once: uploads stamp sites: [scope],
and the asset dialog toggles the rest. Identical files dedupe to one asset
document in Sanity, so the scope is merged rather than overwritten.
Configuration
| Option | Default | Description |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------------- |
| scope | – | Enables per-workspace scoping (see below). Omit for a single shared library. |
| folderType | mediaFolder | Document type used for folders. |
| toolName | media | Tool name, and the Studio URL segment. |
| toolTitle | Media | Tool title in the navbar. |
| assetSource | replace | replace hides the built-in image sources, append keeps them, false registers none. |
| apiVersion | 2025-02-19 | API version for the Studio client. |
scope
| Option | Default | Description |
| ------------- | ------- | ---------------------------------------------------------------------------- |
| value | – | Required. The scope this workspace browses and uploads into. |
| fallback | – | Scope that assets with no scope field belong to. Set it on the legacy scope. |
| options | [] | All scopes, listed in the asset dialog so an asset can be reassigned. |
| assetField | sites | Array field stamped on sanity.imageAsset. |
| folderField | site | String field stamped on folder documents. |
Data model
- Folders are documents (
mediaFolderby default) whosetitleholds the folder's full path —brand/logos. An asset points at one through the same path string in itsfolderfield. Folders are documents rather than a derived list so an empty folder can be created and persists. - Nesting is a materialized path, not a parent reference. That keeps listing and filtering to plain string comparisons — GROQ cannot express recursive traversal — and means a flat folder created before nesting is already a valid one-segment path, so there is nothing to migrate. The cost is that renaming or moving a folder has to re-point its descendants, which the plugin does in a single transaction.
- Intermediate folders can be implied. An asset in
brand/logosputsbrandon screen even when no folder document exists for it, so a path is never unreachable. Creatingbrand/logosdoes not create abranddocument. - Folder names cannot contain
/; nesting comes from creating a folder while inside another one. - Assets are untouched
sanity.imageAssetdocuments. Scoping adds an array field (sites) and organizing addsfolder; nothing else is duplicated, so the assets stay usable by any query or plugin. - Deletion is reference-safe. Before deleting, the plugin counts incoming references per asset and only removes the unreferenced ones, reporting how many were kept. Drafts and release versions count as references. Deleting a folder cascades through its nested folders, and the confirmation says how many folders and images that adds up to. Deletion is a hard delete — Sanity removes the underlying file, so there is no undo beyond your project's history retention and backups.
Notes
- Native (drag-onto-the-field) uploads bypass the plugin and are therefore
unscoped and unfoldered. Assign them from the Media tool, or set
assetSource: "replace"(the default) so browsing always goes through the library. - Search is recursive: inside a folder it matches assets in that folder and everything nested below it, since a search that stopped at the current level would miss most of a nested library. Browsing without a search term stays scoped to the folder's own contents.
- Folder documents appear in the default document type list of
structureTool. Filter them out in a customstructureif you don't want editors there — the plugin already removes the type from the global "create new" menu. - Drafts and releases protect an asset. Deletion checks and the "Used by"
tab count every document that references an asset — published, drafts and
release versions alike — so an image used only in an unpublished draft is
never deleted by cleanup. These reads run under the
rawperspective for exactly that reason; apublishedperspective reports a draft-only asset as unreferenced. The tab marks such documentsUnpublished, so it is clear why an asset was kept.
Development
bun install
bun run check # typecheck
bun run build # emit dist/
bun testReleases run on changesets: add one with bun run changeset in every PR that
changes src/. Merging to main opens (or updates) a release PR; merging that
publishes to npm.
