sanity-plugin-sgntech-review
v1.0.0
Published
Review blocks for Sanity Studio — score, verdict, pros and cons inside an article, with schema.org/Review markup for search results.
Maintainers
Readme
What you get
- A
reviewBlockfor Portable Text: the reviewed item, a score, the verdict in one sentence, and two short lists — for and against. - One scale per Studio, not per block. Range and step live in the plugin configuration, so the reviews of a publication stay comparable instead of drifting between 4/5 and 85/100.
schema.org/Reviewout of the box. The renderer writes the JSON-LD next to the block, withratingValue,bestRating,worstRating,positiveNotesandnegativeNotesfilled in from the same fields the reader sees.- Scores are optional. A review without a number stays a valid review — the markup simply omits
reviewRating. - The author comes from the article; a field on the block covers guest reviews.
- Design-agnostic. The score element carries
data-value,data-maxanddata-percent, so stars, dots or a dial are a matter of your own CSS. The plugin ships structure only.
Installation
npm install sanity-plugin-sgntech-reviewRequires Sanity Studio v5 or v6 and React 18 or 19. No runtime dependencies.
📦 On npm: sanity-plugin-sgntech-review
Usage in the Studio
// sanity.config.ts
import {defineArrayMember, defineConfig, defineField} from 'sanity'
import {reviewBlock} from 'sanity-plugin-sgntech-review'
export default defineConfig({
// ...
plugins: [
reviewBlock({
scale: {min: 1, max: 5, step: 0.5},
itemTypes: ['release', 'concert'], // your own document types — optional
defaultItemType: 'MusicPlaylist',
}),
],
schema: {
types: [
{
type: 'document',
name: 'article',
fields: [
defineField({
type: 'array',
name: 'body',
of: [
defineArrayMember({type: 'block'}),
defineArrayMember({type: 'reviewBlock'}), // ← the review block
],
}),
],
},
],
},
})Options
reviewBlock({
name: 'reviewBlock', // schema type name
title: 'Review', // label in the insert menu
scale: {min: 1, max: 5, step: 0.5}, // one scale for the whole Studio
itemTypes: [], // document types the review may reference
defaultItemType: 'CreativeWork', // preselected schema.org type
allowReviewerOverride: true, // field for a guest reviewer
allowReviewDate: true, // field for datePublished
})Stars in Google — what actually works
Google shows review stars only for a fixed set of itemReviewed types. The Studio dropdown says
which is which:
| Works | Does not get stars |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Event, MusicRecording, MusicPlaylist, Book, Movie, Episode, CreativeWorkSeason, CreativeWorkSeries, Game, SoftwareApplication, Product, MediaObject, Course, Recipe, LocalBusiness, Organization | MusicAlbum, CreativeWork |
So a concert review marked up as Event is eligible. For an album, use MusicPlaylist or
MusicRecording — MusicAlbum produces valid markup that simply never earns stars. Whether Google
then shows them is Google's call; no plugin can promise that.
isStarsEligible(type) is exported if you want to warn editors in your own UI.
Stored value
{
"_type": "reviewBlock",
"_key": "a1b2",
"itemName": "Zeit",
"itemType": "MusicPlaylist",
"itemUrl": "https://example.com/zeit",
"score": 4.5,
"verdict": "The best sounding record the band has made.",
"positives": ["Sound", "Two strong singles"],
"negatives": ["Three fillers in the middle"],
"reviewDate": "2026-07-14"
}Rendering in your frontend
import {PortableText} from '@portabletext/react'
import {createReviewPortableTextComponents} from 'sanity-plugin-sgntech-review/render'
const components = createReviewPortableTextComponents({
scale: {min: 1, max: 5, step: 0.5}, // the same scale as in the Studio
author: {name: article.author.name},
publisher: {name: 'My Magazine'},
})
;<PortableText value={article.body} components={components} />Referenced documents need a resolver, since only your site knows its own fields and routes:
createReviewPortableTextComponents({
resolveReference: (item) => ({
name: String(item.title ?? ''),
url: `/releases/${(item.slug as {current?: string})?.current}`,
}),
})Fetch the reference along with the block:
body[]{..., _type == "reviewBlock" => {..., item->{title, slug}}}Props
| Prop | Default | Purpose |
| ---------------------- | ------------ | --------------------------------------------------- |
| value | — | The reviewBlock value |
| scale | 1–5 | Must match the Studio configuration |
| author / publisher | — | Used in the markup; the block's reviewer field wins |
| resolveReference | — | Turns a referenced document into name, url, image |
| jsonLd | true | Emit the JSON-LD next to the block |
| datePublished | — | Fallback when the block carries no date |
| formatScore | plain digits | e.g. (score) => score.toLocaleString('de-DE') |
| showItem | true | Show the name of the reviewed item |
| injectStyles | true | Add the structural stylesheet |
| labels | English | score, positives, negatives, by, reviewOf |
JSON-LD somewhere else
import {reviewJsonLd} from 'sanity-plugin-sgntech-review/render'
const data = reviewJsonLd(block, {scale, author, publisher})
// render it in <head> yourself
;<Review value={block} jsonLd={false} />Styling
.sgn-review {
--sgn-review-gap: 0.75rem;
--sgn-review-meta-size: 0.875em;
--sgn-review-meta-opacity: 0.75;
--sgn-review-meter-height: 0.4rem;
--sgn-review-meter-track: color-mix(in srgb, currentColor 15%, transparent);
--sgn-review-meter-fill: currentColor;
--sgn-review-notes-gap: 1.25rem;
}Class names: sgn-review, __item, __score, __meter, __meter-fill, __verdict, __notes,
__note-group, __note-title, __note-list, __byline, __link. The wrapper carries
data-has-score, note groups carry data-tone="positive" / "negative".
Turning the score into stars needs no markup change — the percentage is right there:
.sgn-review__score {
position: relative;
font-size: 0;
}
.sgn-review__score::before {
content: '★★★★★';
font-size: 1.25rem;
opacity: 0.25;
}
.sgn-review__score::after {
content: '★★★★★';
font-size: 1.25rem;
position: absolute;
left: 0;
top: 0;
overflow: hidden;
/* width from data-percent, set via a small inline style or a CSS attr() polyfill */
}Note that data-percent is the score's position on the scale: on a 1–5 scale, a 1 is 0 % — the
bottom of the scale, not a fifth of it.
Accessibility
- The score is written out as text; the meter is decorative and hidden from screen readers, so the value is announced once rather than twice.
- For and against are real lists with headings, not a run-on sentence.
- The block carries an accessible name built from the reviewed item.
Develop
npm install
npm test # unit tests for the scale, the score and the JSON-LD
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
