sanity-plugin-sgntech-setlist
v1.0.0
Published
Setlist blocks for Sanity Studio — the songs of a show in columns, encores as their own section, numbered straight through.
Maintainers
Readme
What you get
- A
setlistBlockfor Portable Text: the songs of an evening, grouped into sections — main set, encore, a second encore, an acoustic block. - Numbers are never stored. They are counted across all sections when rendering, so the first encore song continues at 16 instead of starting at 1, and inserting a song renumbers everything below it.
- Markers per song — live premiere, cover, acoustic, snippet, guest, aborted — plus a free note for the thing no list foresees. Cover and guest get their own field, so "Cover: Depeche Mode" is data, not prose.
- Columns instead of an endless list. Twenty songs read down the first column, then down the second, the way a printed setlist works. Column count is a CSS variable; narrow screens fall back to one column on their own.
- Optional header with artist, date, venue and city — typed in, or pulled from your own concert documents.
sanity-plugin-sgntech-setlist/render— the React renderer, without anysanityimport.
Installation
npm install sanity-plugin-sgntech-setlistRequires Sanity Studio v5 or v6 and React 18 or 19. No runtime dependencies.
📦 On npm: sanity-plugin-sgntech-setlist
Usage in the Studio
// sanity.config.ts
import {defineArrayMember, defineConfig, defineField} from 'sanity'
import {setlist} from 'sanity-plugin-sgntech-setlist'
export default defineConfig({
// ...
plugins: [setlist({concertType: 'concert'})], // concertType is optional
schema: {
types: [
{
type: 'document',
name: 'concertReport',
fields: [
defineField({
type: 'array',
name: 'body',
of: [
defineArrayMember({type: 'block'}),
defineArrayMember({type: 'setlistBlock'}), // ← the setlist
],
}),
],
},
],
},
})The plugin registers three types: setlistBlock, setlistSection and setlistSong.
Options
setlist({
name: 'setlistBlock', // block type name
title: 'Setlist', // label in the insert menu
sectionType: 'setlistSection',
songType: 'setlistSong',
concertType: undefined, // e.g. 'concert' — adds a reference field for the header
allowHeader: true, // artist, date, venue, city
})The Original artist and Guest fields appear only once the matching marker is ticked, so the form stays short for the twenty songs that are just songs.
Stored value
{
"_type": "setlistBlock",
"artist": "A Band",
"date": "2026-07-15",
"venue": "Waldbühne",
"city": "Berlin",
"sections": [
{
"_key": "main",
"title": "Main set",
"songs": [
{"_key": "s1", "title": "Opening song"},
{"_key": "s2", "title": "Stripped", "markers": ["cover"], "coverOf": "Depeche Mode"},
{"_key": "s3", "title": "Last one", "markers": ["aborted"], "note": "cut after two lines"}
]
},
{"_key": "enc", "title": "Encore", "songs": [{"_key": "s4", "title": "The hit"}]}
]
}Rendering in your frontend
import {PortableText} from '@portabletext/react'
import {createSetlistPortableTextComponents} from 'sanity-plugin-sgntech-setlist/render'
const components = createSetlistPortableTextComponents({
artistAs: 'h3',
formatDate: (date) => date.toLocaleDateString('en-GB', {dateStyle: 'long'}),
})
;<PortableText value={report.body} components={components} />With a referenced concert, tell the renderer how to read your document:
createSetlistPortableTextComponents({
resolveConcert: (concert) => ({
artist: String(concert.artistName ?? ''),
venue: String(concert.venue ?? ''),
city: String(concert.city ?? ''),
date: String(concert.date ?? ''),
}),
})body[]{..., _type == "setlistBlock" => {..., concert->{artistName, venue, city, date}}}Props
| Prop | Default | Purpose |
| ---------------- | ------------- | ----------------------------------------------------------- |
| value | — | The setlistBlock value |
| resolveConcert | — | Turns a referenced concert into the header |
| showHeader | true | Show artist, venue, city and date |
| formatDate | stored string | Formats the date of the show |
| artistAs | p | Element for the artist line |
| injectStyles | true | Add the structural stylesheet |
| labels | English | Marker texts, coverOf, withGuest, source, setlistOf |
| className | — | Extra class on the wrapper |
Helpers
import {numberSetlist, songCount} from 'sanity-plugin-sgntech-setlist/render'
songCount(value.sections) // 19
numberSetlist(value.sections) // sections with numbered songsStyling
.sgn-setlist {
--sgn-setlist-columns: 2;
--sgn-setlist-column-width: 16rem; /* below this width, one column */
--sgn-setlist-column-gap: 2rem;
--sgn-setlist-row-gap: 0.35rem;
--sgn-setlist-section-gap: 1.25rem;
--sgn-setlist-number-width: 2.2em;
--sgn-setlist-meta-size: 0.875em;
--sgn-setlist-meta-opacity: 0.7;
}The variables have no declaration of their own inside the plugin — the defaults sit at the point of
use. That means setting them on any ancestor works, not only on .sgn-setlist itself:
.article--print .sgn-setlist,
.article--print {
--sgn-setlist-columns: 3;
}Class names: sgn-setlist, __header, __artist, __facts, __fact, __section,
__section-title, __songs, __song, __number, __title, __markers, __marker, __note,
__footer, __link.
Every song carries data-markers with the raw marker names, so badges and colours need no extra
markup:
.sgn-setlist__song[data-markers~='premiere'] .sgn-setlist__markers {
font-weight: bold;
}Accessibility
- The songs are an ordered list; the printed numbers come from the
valueattribute, so screen readers announce the same positions readers see. - Markers are text, not icons — nothing depends on recognising a symbol.
- Column layout is pure CSS, so reading order and DOM order stay identical.
Develop
npm install
npm test # unit tests for numbering, markers and the header
npm run lint
npm run build
npm run link-watch # build + publish to a local yalc repo for Studio testingBuilt with @sanity/plugin-kit.
License
MIT © SGNTech
