npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@maciejtrzcinski/sanity-plugin-section-builder

v1.0.0

Published

Visual page-builder section picker for Sanity Studio — thumbnail previews in the array editor, edit pane, and insert menu, driven by a single section registry.

Readme

sanity-plugin-section-builder

Visual page-builder section picker for Sanity Studio. Auto-detect your sections from the schema (or list them explicitly) and get thumbnail previews everywhere:

  • in the array editor (each section item shows its thumbnail),
  • in the "Edit X Section" pane,
  • in the native insert menu grid.

No more copy-pasting components: {preview: …} into every section schema — the plugin attaches the preview globally, keyed by schema type name. Preview images resolve by filename convention, so there's nothing to map by hand.

Install

npm install @maciejtrzcinski/sanity-plugin-section-builder

Requires sanity ≥ 3.36 (for the insert-menu grid previewImageUrl API) and react 18 or 19 (both are peer dependencies — they come from your studio).

Usage

1. Define your sections

Either auto-detect them from your schema (recommended) or list them explicitly.

Auto-detect (recommended)

Drop in a *Section object type and it's picked up automatically — no registry to maintain. Preview filenames default to the kebab-cased type name (heroSectionhero-section.png); see Preview images for how resolution works.

// sections.ts
import type {SectionBuilderConfig} from '@maciejtrzcinski/sanity-plugin-section-builder'
import {schemaTypes} from './schemaTypes'

export const SECTIONS = {
  previewBaseUrl: '/static/section-previews',
  detect: {
    types: schemaTypes,
    // isSection defaults to (t) => t.name.endsWith('Section')
    include: ['quoteSectionTitled'], // sections that don't match the predicate
    order: ['heroSection'], // pin to the top; sort: 'name' for the rest
  },
} satisfies SectionBuilderConfig

detect options: isSection, include, exclude, previewImage (deriver), group, overrides, order, sort ('name' or a comparator). order is applied after sort, so you can alphabetize everything and still pin a few to the front. You can also call detectSections(types, options) directly to build the list and tweak it before passing it as sections.

Explicit

export const SECTIONS = {
  previewBaseUrl: '/static/section-previews',
  sections: [
    {type: 'heroSection', previewImage: 'hero.png'},
    {type: 'faqSection', previewImage: 'faq.png'},
    {type: 'blogIndexSection', group: 'index'}, // no preview yet — that's fine
  ],
} satisfies SectionBuilderConfig

previewImage may be a bare filename (resolved against previewBaseUrl) or an absolute URL / root-relative path used as-is.

Preview images

There's no hand-maintained map from section → image — the image is resolved by filename convention: ${toKebabCase(type)}.png under previewBaseUrl. Name a section heroSection and drop hero-section.png in the folder; it just appears.

Only reference files that actually exist. The browser can't check the filesystem, so the default deriver returns a filename for every section. That's fine for the array-item / edit-pane preview (a missing image hides itself, no broken icon) — but the native insert-menu grid can't hide a broken image. So the robust pattern is to feed the deriver a set of the files that exist, generated from the folder at build time (a "manifest"):

// preview-manifest.ts — generated by a tiny script that reads the folder, e.g.
//   node -e "…readdirSync('static/section-previews')…" > committed file
export const PREVIEW_FILES = new Set<string>([
  'hero-section.png',
  'faq-section.png',
  // …
])
import {toKebabCase} from '@maciejtrzcinski/sanity-plugin-section-builder'
import {PREVIEW_FILES} from './preview-manifest'

detect: {
  types: schemaTypes,
  previewImage: (name) => {
    const file = `${toKebabCase(name)}.png`
    return PREVIEW_FILES.has(file) ? file : undefined // undefined → no preview, not a 404
  },
}

A section with no matching file simply gets no thumbnail — everywhere, cleanly.

2. Register the plugin

// sanity.config.ts
import {defineConfig} from 'sanity'
import {sectionBuilder} from '@maciejtrzcinski/sanity-plugin-section-builder'
import {SECTIONS} from './sections'

export default defineConfig({
  // …
  plugins: [sectionBuilder(SECTIONS)],
})

3. Build your page-builder field

import {defineType} from 'sanity'
import {sectionField} from '@maciejtrzcinski/sanity-plugin-section-builder'
import {SECTIONS} from './sections'

export const page = defineType({
  name: 'page',
  type: 'document',
  fields: [sectionField(SECTIONS, {group: 'content'})],
})

// One registry can power several fields via a filter:
sectionField(SECTIONS, {name: 'indexBuilder', filter: (s) => s.group === 'index'})

That's it. Every registered type now renders with its thumbnail, and the insert menu shows a visual grid.

Migrating an existing studio

If your section schemas already wire previews per type, the move is mechanical:

  1. Register the plugin (step 2 above). The global form.components.preview resolver now renders the thumbnail for every registered section type.
  2. Delete the per-schema wiring — remove the components: {preview: …} line and its import from each section's defineType. A one-line codemod over your section files handles all of them at once. (Renderer/preview no longer needed per file.)
  3. Replace any hand-rolled page-builder field with sectionField(SECTIONS, …) (step 3). It builds the array of members and wires the insert-menu grid for you.
  4. Name preview images by convention${toKebabCase(type)}.png — or keep your existing names by setting previewImage per section (explicit mode) or via detect.previewImage / overrides.

The data schema is unaffected (preview components aren't part of the schema), so sanity schema extract output stays byte-for-byte identical.

Guard against missing assets (CI / tests)

If you gate previewImage by the folder manifest (above), there are no broken references by construction. auditPreviewAssets is still handy to surface unused files (dead assets) and sections that still need a thumbnail — and it's pure, so it stays browser-safe (no node:fs in the bundle). Pass it the resolved section list and the files on disk; build the list with detectSections when you use auto-detect (the config's sections is only populated in explicit mode):

import {readdirSync} from 'node:fs'
import {auditPreviewAssets, detectSections} from '@maciejtrzcinski/sanity-plugin-section-builder'
import {schemaTypes} from './schemaTypes'

const sections = detectSections(schemaTypes, {include: ['quoteSectionTitled']})
const {missing, unused, withoutPreview} = auditPreviewAssets(
  sections,
  readdirSync('static/section-previews'),
)

expect(missing).toEqual([]) // every referenced filename exists on disk
// `unused`         — preview files no section references (dead assets)
// `withoutPreview` — sections that still need a thumbnail

API

| Export | Entry | Purpose | | --------------------------------------------- | ----- | ----------------------------------------------------------------------------- | | sectionBuilder(config) | . | Studio plugin — globally attaches the preview thumbnail. | | sectionField(config, opts?) | . | Builds the page-builder array field + insert-menu grid. | | sectionArrayMembers(config, filter?) | . | Just the defineArrayMember[] (if you build the field yourself). | | detectSections(types, options?) | . | Derive the section list from schema types (predicate + order/sort/overrides). | | toKebabCase(name) | . | The default preview-filename deriver. | | makeSectionItemPreview(map, height) | . | Builds the per-type preview component (the plugin uses this internally). | | SectionPreviewImage({src, height}) | . | The thumbnail <img> (hides itself on load error). | | auditPreviewAssets(sections, files) | . | Pure check for missing/unused preview assets (feed it readdirSync(...)). | | resolvePreviewUrl, sectionsMissingPreview | . | Helpers used internally; exported for convenience. |

Exported types: SectionBuilderConfig, SectionDefinition, DetectConfig, DetectSectionsOptions, SchemaTypeLike, SectionFilter, SectionFieldOptions, ResolvedSectionBuilderConfig, AssetAuditResult.

SectionDefinition

interface SectionDefinition {
  type: string // schema type name of the section object
  previewImage?: string // filename | absolute URL | root-relative path
  group?: string // optional grouping for filtered fields
}

SectionBuilderConfig

interface SectionBuilderConfig {
  sections?: readonly SectionDefinition[] // explicit list, OR…
  detect?: DetectConfig // …auto-detect from schema types
  previewBaseUrl?: string // default '/static/section-previews'
  previewHeight?: number // default 320
}

type DetectConfig = {
  types: ReadonlyArray<{name: string; type?: string}>
  isSection?: (def) => boolean // default: name.endsWith('Section')
  include?: string[] // force-include non-matching types
  exclude?: string[] // drop matching types
  previewImage?: (name) => string | undefined // default: `${kebab(name)}.png`
  group?: (name) => string | undefined
  overrides?: Record<string, Partial<Omit<SectionDefinition, 'type'>>>
  order?: string[] // pin to front (after sort)
  sort?: 'name' | ((a, b) => number)
}

SectionFieldOptions (second arg to sectionField)

interface SectionFieldOptions {
  name?: string // default 'pageBuilder'
  title?: string // default 'Page Builder'
  group?: string // fieldset/group the field belongs to
  filter?: (s: SectionDefinition) => boolean // restrict which sections are offered
  fieldOptions?: Partial<ArrayDefinition> // merged into the resulting defineField
}

Why a plugin (vs. native insert menu)?

Sanity's native insert menu already supports a grid with previewImageUrl — and this plugin wires that for you. Its extra value is auto-detection + convention-based previews (no registry, no per-section image map) and the thumbnail on the array item and edit pane, which the native insert menu does not cover, all without per-schema boilerplate.

How it works

  • Previews: registered via Sanity's global form.components.preview resolver, which keys off the schema type name — so no per-schema component is needed.
  • Insert-menu grid: wired through the array field's options.insertMenu (previewImageUrl), built for you by sectionField.
  • Section list: detect filters your schema types by a predicate at config time; nothing is read from the running dataset.

The package is browser-safe — it pulls in no node: modules, so it bundles cleanly into the Studio.

Developing

Requires Node ≥ 20 and pnpm (packageManager is pinned in package.json).

pnpm install
pnpm build        # bundle ESM + CJS + d.ts into dist/ (pkg-utils)
pnpm watch        # rebuild on change
pnpm test         # vitest
pnpm type-check   # tsc --noEmit
pnpm check        # lint + type-check + format:check + knip + test (CI gate)

Releases are managed with Changesets: run pnpm changeset to record a change, then pnpm release publishes.

License

MIT © Maciej Trzciński