A collection of utilities formulated to provide positive experiences in the Sanity Studio.
Note: these pills are non-prescription—use them as they fit your needs and ignore them as they don’t!
Documentation on each of the utilities provided follow.
The fields
helper allows authoring of field schemas in a more succinct,
scannable manner. By way of example:
import { fields } from "sanity-pills"
{
name: "example",
type: "object",
fields: fields({
// a required string
name: { required: true },
// an optional number
age: {
type: "number",
},
// a string that is only required when `age` is below 18
guardianName: {
required: ({ parent }) => parent.age < 18,
},
}),
}
This pill will use the object key as the name of the field, and if no title is provided in the value definition, a title will be inferred from the name (similar to default Sanity Studio behavior, less the warnings).
This behaves more or less expected, converting the object into an array with the
one notable callout being that required
relies on the validation
property on
the field and specifying validation
explicitly will override the required
functionality (i.e. Sanity Pills will not merge them).
There are no built-in validators for image dimensions in the Sanity Studio, but
these are often valuable. It is straightforward enough to parse the image
dimensions out of the image _id
field, but Sanity Pills can handle this for
you.
import { createImageField, fields } from "sanity-pills"
export default {
name: "example",
type: "document",
fields: fields({
headshot: createImageField({
validations: {
required: true,
minWidth: 500,
minHeight: 500,
},
warnings: {
minWidth: 1000,
minHeight: 1000,
},
}),
}),
}
This example illustrates a required headshot image that will be invalid unless the original image dimensions are at minimum 500x500. It also shows a warning message suggesting that the best results would be achieved with an image of 1000x1000 when either of the original dimensions is smaller.
This pill also supports validations of maxWidth
and maxHeight
, though they
are less likely to be required.
On a related note, Sanity Pills also exposes the function used to parse a Sanity
asset ID as decodeAssetId
. It's documented later in this document.
If you happen to want your slugs to be valid URL paths, complete with the initial slash and a trailing slash, possibly with a required prefix, while supporting the “generate” button, you might appreciate this pill—it does just that.
It enforces the following rules:
- Slugs are required
- Slugs must start with
/
- Slugs must end with
/
- The first non-slash character has to be a letter, except when the slug is
/404/
- Slugs have to be lowercase alphanumeric characters, plus hyphens and forward slashes
- If the
prefix
option is supplied tocreateSlugField
, the slug must begin with/<prefix>/
Here's are a couple examples:
import { createSlugField, fields } from "sanity-pills"
export default {
name: "example",
type: "document",
fields: fields({
// optional string field
name: {},
// slug with a “generate” button that sources from name
slug: createSlugField({ source: "name" }),
// produces a prefixed URL like /blog/hello/ and validates
// that the prefixes is included as expected
scopedSlug: createSlugField({
prefix: "blog",
source: "name",
}),
}),
}
You can use an async function for source
(it's passed through unchanged), but
for now prefix
only supports static string values.
Oh, and if you want to roll your own slug but want a handy slugify
routine,
you can use createSlug
from the Sanity Pills package. E.g.:
import { createSlug } from "sanity-pills"
export default {
// …
slug: {
type: "slug",
options: {
source: "name",
slugify: createSlug,
},
required: true,
},
}
Portable Text is a powerful way for editors to author non-trivial, rich data structures in a platform agnostic way, but it's easy to wind up with poor-quality content represented impeccably. As an array of nested blocks, enforcing common things like no trailing whitespace, links always having associated URLS, or even just that real content is provided can be cumbersome.
This pill makes validating Portable Text fields easier by including common validation patterns out of the box and supporting custom extensions.
Sanity Pills ships with two built in block validators ready to use: all
and
optional
. Both of these enforce the following validations, and all
also
enforces the presence of a value for the field.
- No empty blocks: an empty paragraph, for example
- No newlines: prevents single blocks (rendered as
<p>
tags) by default from containing newlines (rendered as<br />
tags) - No terminating whitespace: disallows spaces at the beginning or end of a block
- No missing links: links must have a valid
href
property - No unstyled blocks: each block needs a
style
set (e.g.normal
orh1
) - No stacked marks: disallows having text that is bold and italic or italic and strikethrough, etc. while allowing stacking of custom marks
- No marks on headings: disallows any marks to be used on blocks with a
style starting with
h
If you’re happy with this list, you can use either of the default block validators like so:
import { defaultBlockValidators } from "sanity-pills"
{
type: "array",
of: [{ type: "block" }],
validation: defaultBlockValidators.all, // or .optional
}
It's entirely likely that this very opinionated set of validations is not entirely suitable for your use case, or that you need to add an additional custom validator to the list. Not to worry, that's possible like so:
import { createBlockValidator } from "sanity-pills"
const yourValidator = createBlockValidator({
// enable a couple of the built in validations
noEmptyBlocks: true,
validateLinks: true,
// add a custom validation that only allows heading blocks
noTextAllowed: (blocks) => {
const errorPaths = (blocks || [])
.filter(
(block) =>
block._type === "block" &&
block.style.match(/^h[1-6]$/)
)
.map((block, index) => [{ _key: block._key }] || [index])
return (
errorPaths.length === 0 || {
message: "Must be styled as a heading",
paths: errorPaths,
}
)
}
})
// use your validator like so
{
type: "array",
of: [{ type: "block" }],
validation: yourValidator,
}
Since block content is stored as an array, you can't use it directly when
customizing previews. Instead you have to convert it to a string, but ignore
everything that isn't readily convertible to a string. That's what
blockPreview
does:
import { blockPreview } from "sanity-pills"
export default {
// …
preview: {
select: {
title: "title",
copy: "copy",
},
prepare: ({ title, copy }) => ({
title,
subtitle: blockPreview(copy),
}),
},
}
Sanity assigns stable, informative IDs for asset uploads, including the format
and, for images, the dimensions of the original file. These can be easily parsed
using the decodeAssetId
export from Sanity Pills.
import { decodeAssetId } from "sanity-pills"
const {
dimensions: { width, height },
format,
} = decodeAssetId(someImageAssetId)
Arrays of references are pretty common, and the usual cases for them typically only expect a single instance of any selected document. This wee routine enhances the experience of choosing references by removing any documents that are already in the array.
import { noDuplicateRefs } from "sanity-pills"
const field = {
type: "array",
of: [
{
type: "reference",
to: [{ type: "someDocumentType" }],
options: {
filter: noDuplicateRefs,
},
},
],
}
Typical path joining routine that prevents doubling up slashes but won't remove doubled slashes in a string you pass in.
import { urlJoin } from "sanity-pills"
urlJoin("/foo/", "/bar/") // #=> "/foo/bar/"
urlJoin("foo", "bar") // #=> "foo/bar"
urlJoin("/f/o/o/", "/b/a/r/") // #=> "/f/o/o/b/a/r/"
urlJoin("/", "/", "/", "/", "/") // #=> "/"
Copyright ©2022 Corey Ward. Available under the MIT License.