sanity-plugin-sgntech-release
v1.1.0
Published
Album, EP and single blocks for Sanity Studio — cover, facts and tracklist inside an article, with placeholders for tracks that have not been announced yet.
Maintainers
Readme
What you get
- A
releaseEmbedblock for Portable Text: cover on one side, title, artist, kind of release and label on the other, tracklist underneath. - Half-known tracklists. Every position is listed; tracks that have not been announced are marked as such and render as a placeholder bar. Two singles out and the rest under wraps is a normal state, not a broken one.
- The release state comes from the date, never from a checkbox: a date in the future reads "out on", a date in the past reads "released", a rough date like
Spring 2027reads "announced for", nothing at all reads "to be announced". Nobody has to flip anything on release day. - Tracklist optional per embed. A switch on the block decides whether the list shows up — the same album can appear with its full tracklist in one article and as a bare cover card in another.
- Optional link to the release page. Either a URL typed in the Studio or one built by your own routing, applied to cover and title.
- A
releasedocument type, so one album is maintained in one place and every article embedding it updates at once — or type the fields straight into the block for a one-off mention. - Optional reference to your own artist documents; plain text otherwise.
sanity-plugin-sgntech-release/render— the React renderer. That entry point does not importsanity, so your app bundle stays clean.- Design-agnostic. The stylesheet only carries the two-column frame and the list — no colours, no fonts. Everything else is yours.
Installation
npm install sanity-plugin-sgntech-releaseRequires Sanity Studio v5 or v6 and React 18 or 19. No runtime dependencies.
📦 On npm: sanity-plugin-sgntech-release
Usage in the Studio
// sanity.config.ts
import {defineArrayMember, defineConfig, defineField} from 'sanity'
import {releaseEmbed} from 'sanity-plugin-sgntech-release'
export default defineConfig({
// ...
plugins: [releaseEmbed()],
schema: {
types: [
{
type: 'document',
name: 'article',
fields: [
defineField({
type: 'array',
name: 'body',
of: [
defineArrayMember({type: 'block'}),
defineArrayMember({type: 'releaseEmbed'}), // ← the release block
],
}),
],
},
],
},
})The plugin registers three types: the release document, the releaseEmbed block and the
releaseTrack object used by both.
Options
releaseEmbed({
releaseType: 'release', // document type name
blockType: 'releaseEmbed', // block type name
trackType: 'releaseTrack', // track object type name
artistType: undefined, // e.g. 'band' — adds a reference field for the artist
registerDocumentType: true, // false if your Studio already has a release type
allowReference: true, // offer the reference field on the block
allowInline: true, // offer the inline fields on the block
disableNew: false, // cover: pick from the Media Library only
})Editors get two switches on the block itself: Show tracklist and an optional Link to the release page.
Reference or inline
The block offers both. Picking a release hides the inline fields, so the same album is never maintained twice in one block. When both carry a value, the reference wins — field by field, so an inline value can still fill a gap the referenced document leaves open.
Stored value
{
"_type": "releaseEmbed",
"_key": "a1b2",
"title": "Unnamed album",
"artistName": "A Band",
"type": "album",
"label": "Indie Records",
"releaseDate": "2027-03-05",
"releaseDateText": "Spring 2027",
"cover": {"_type": "image", "asset": {"_ref": "image-…"}, "alt": "Cover"},
"tracks": [
{"_key": "t1", "title": "First single", "isSingle": true, "duration": "3:12"},
{"_key": "t2", "unknown": true},
{"_key": "t3", "title": "Second single", "featuring": ["Guest singer"], "isSingle": true}
]
}Rendering in your frontend
Fetch the block with the cover and — if used — the reference and the artist dereferenced:
body[]{
...,
_type == "releaseEmbed" => {
...,
cover{..., asset->{url, altText}},
release->{..., slug, cover{..., asset->{url, altText}}, artist->{name}}
}
}With @portabletext/react
import {PortableText} from '@portabletext/react'
import {releasePortableTextComponents} from 'sanity-plugin-sgntech-release/render'
;<PortableText value={article.body} components={releasePortableTextComponents} />Standalone component
import {Release} from 'sanity-plugin-sgntech-release/render'
;<Release value={block} titleAs="h2" formatDate={(date) => date.toLocaleDateString('en-GB')} />With @sanity/image-url or next/image
import imageUrlBuilder from '@sanity/image-url'
import Image from 'next/image'
const builder = imageUrlBuilder(client)
;<Release
value={block}
resolveImageUrl={(cover) => builder.image(cover).width(600).auto('format').url()}
renderCover={({src, alt}) => <Image src={src!} alt={alt} width={600} height={600} />}
/>Props
| Prop | Default | Purpose |
| ----------------- | ------------ | ---------------------------------------------------------- |
| value | — | The releaseEmbed value |
| titleAs | p | Element for the title — h2, h3, h4 or p |
| showTracklist | true | Render the track list |
| formatDate | ISO date | Formats the exact release date |
| now | current date | Reference point for released vs upcoming — handy for tests |
| resolveImageUrl | — | Build the cover URL yourself |
| renderCover | — | Replace the <img> element |
| injectStyles | true | Add the structural stylesheet |
| labels | English | All visible texts, see below |
| className | — | Extra class on the wrapper |
Labels
<Release
value={block}
labels={{
types: {album: 'Album', ep: 'EP', single: 'Single', compilation: 'Compilation', live: 'Live'},
released: (date) => `Released ${date}`,
upcoming: (date) => `Out ${date}`,
announced: (text) => `Announced for ${text}`,
unknown: 'Release date to be announced',
featuring: (artists) => `feat. ${artists.join(', ')}`,
single: 'Single',
unknownTrack: 'Title not announced yet',
tracklist: 'Tracklist',
}}
/>Linking to the release page
An explicit URL in the Studio always wins. Without one, resolveHref builds the link — that is
where your site's routing lives, since a plugin cannot know it:
import Link from 'next/link'
import {Release, slugOf} from 'sanity-plugin-sgntech-release/render'
;<Release
value={block}
resolveHref={(release) => `/releases/${slugOf(release)}`}
renderLink={({href, className, children}) => (
<Link href={href} className={className}>
{children}
</Link>
)}
/>Absolute http(s) links open in a new tab with rel="noreferrer noopener"; relative ones stay in
the tab. Pass linkCover={false} to link only the title.
Styling
.sgn-release {
--sgn-release-cover-width: 12rem;
--sgn-release-gap: 1.25rem;
--sgn-release-radius: 0;
--sgn-release-breakpoint: 30rem; /* below this the cover moves above the facts */
--sgn-release-meta-size: 0.875em;
--sgn-release-meta-opacity: 0.75;
--sgn-release-placeholder: color-mix(in srgb, currentColor 18%, transparent);
--sgn-release-placeholder-height: 0.7em;
}Class names: sgn-release, __head, __cover, __meta, __title, __artist, __facts,
__fact, __status, __tracks, __track, __track-title, __featuring, __duration,
__single, __placeholder, __link. The wrapper carries data-state (released, upcoming, announced,
unknown) and data-type, so a release that is already out can look different without extra
markup.
Accessibility
- Placeholder bars are decorative; screen readers get the words instead ("Title not announced yet"), so a half-known tracklist is not read as a row of nothing.
- The bar width is derived from the track number, not random — server and browser render the same thing, which keeps hydration quiet.
- Alt text on the cover is required in the Studio and falls back to "«Title» by «Artist»".
- The tracklist is an ordered list, so positions are announced.
Develop
npm install
npm test # unit tests for status, tracks, cover and merging
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
