All skills
sanity-io avatar

/sanity-best-practices

@fc8116b official
by Sanitysanity-io/agent-toolkit187 stars
30

Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, webhooks, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, event-driven content automation, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.

Use this Skill: https://skilld.dev/gh/sanity-io/agent-toolkit/sanity-best-practices

This session only. Nothing lands on disk.

referencesmigration-html-import.md

≈1.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Import HTML to Portable Text

Use @portabletext/block-tools with JSDOM to convert HTML from legacy CMSs to Portable Text.

Setup

npm install @portabletext/block-tools jsdom

Basic Conversion

import { htmlToBlocks } from '@portabletext/block-tools'
import { JSDOM } from 'jsdom'

// Get block content type from your schema
const blockContentType = schema.get('blockContent')

const blocks = htmlToBlocks(htmlString, blockContentType, {
  parseHtml: html => new JSDOM(html).window.document,
})

Custom Deserializers

Handle specific HTML patterns:

const blocks = htmlToBlocks(htmlString, blockContentType, {
  parseHtml: html => new JSDOM(html).window.document,
  rules: [
    {
      deserialize(el, next, block) {
        // Custom link handling — links are inline annotations, not blocks.
        // Return an `__annotation` with a `markDef`, and recurse into the
        // child nodes via `next()` so the link text is preserved.
        if (el.tagName?.toLowerCase() === 'a') {
          const href = el.getAttribute('href')
          // An anchor with no `href` (named anchors, JS-driven links) isn't a
          // link. Fall through so the text survives without a dangling markDef.
          if (!href) return undefined
          return {
            _type: '__annotation',
            markDef: {
              _type: 'link',
              href,
              blank: el.getAttribute('target') === '_blank'
            },
            children: next(el.childNodes)
          }
        }
        // Custom image handling — block-level types are wrapped with `block()`
        if (el.tagName?.toLowerCase() === 'img') {
          const src = el.getAttribute('src')
          // Skip sourceless images rather than emitting `image@null`, which
          // the importer reports as a failed asset with no pointer to the node.
          if (!src) return undefined
          return block({
            _type: 'image',
            // NDJSON + `sanity datasets import` only — see the note below.
            _sanityAsset: `image@${src}`
          })
        }
        return undefined  // Fall through to default handling
      }
    }
  ]
})

_sanityAsset is only resolved by sanity datasets import. The NDJSON importer fetches each image@<url> and swaps in a real asset reference. The mutation API does not interpret the directive, so the same blocks written through @sanity/client, sanity exec, or defineMigration are stored verbatim — leaving an image field with a stray _sanityAsset string and no asset reference. On those paths, upload the image first and emit an asset reference instead, as in Image Upload below.

Pre-Processing HTML

Clean HTML before conversion:

function cleanHtml(html) {
  const dom = new JSDOM(html)
  const doc = dom.window.document
  
  // Remove layout elements
  doc.querySelectorAll('header, footer, nav, .sidebar').forEach(el => el.remove())
  
  // Extract metadata before processing body
  const title = doc.querySelector('title')?.textContent
  const description = doc.querySelector('meta[name="description"]')?.content
  
  return {
    body: doc.body.innerHTML,
    metadata: { title, description }
  }
}

Image Upload

Don't just link external images—upload them:

async function uploadImage(client, imageUrl) {
  const response = await fetch(imageUrl)
  const buffer = await response.arrayBuffer()
  
  const asset = await client.assets.upload('image', Buffer.from(buffer), {
    filename: imageUrl.split('/').pop()
  })
  
  return {
    _type: 'image',
    asset: { _type: 'reference', _ref: asset._id }
  }
}

Using in a Migration

Wrap this in defineMigration for controlled imports. This path writes through the mutation API, so any custom rules used here must emit uploaded asset references rather than _sanityAsset directives:

// migrations/import-wordpress-posts/index.ts
import {defineMigration, create} from 'sanity/migrate'
import {htmlToBlocks} from '@portabletext/block-tools'

export default defineMigration({
  title: 'Import WordPress posts',
  async *migrate(documents, context) {
    const posts = await fetchWordPressPosts() // Your import source
    
    for (const post of posts) {
      const blocks = htmlToBlocks(post.content, blockContentType, {
        parseHtml: html => new JSDOM(html).window.document,
      })
      
      yield create({
        _type: 'post',
        title: post.title,
        slug: {_type: 'slug', current: post.slug},
        legacyId: String(post.id),
        body: blocks,
      })
    }
  }
})

Let Sanity generate document IDs for ordinary imported content. Add schema fields for legacy identifiers or slugs, then use GROQ lookups against those fields when you need to rerun an import, patch existing documents, or create references between imported records. Set _id directly only for singleton documents.

Run with: sanity migrations run import-wordpress-posts --no-dry-run

Reference: Schema and Content Migrations

Source: SKILL.md on GitHub

2 warnings14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides comprehensive Sanity.io development best practices, covering schema design, GROQ queries, and integration with major frontend frameworks. It promotes secure development habits, such as proper management of API tokens and environment variables.

  • Socket14d

    1 alert: gptAnomaly

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    8/24 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at fc8116b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • Next.js
  • Nuxt
  • sanity
  • groq
  • schema
  • typegen
  • portable-text
  • visual-editing
  • astro
  • remix
  • sveltekit
  • migrations
  • localization
  • cms

README badge

README badge for sanity-io/agent-toolkit/sanity-best-practices

Provides guidelines and reference materials for Sanity schema design, GROQ queries, TypeGen, Visual Editing, Portable Text, Studio structure, localization, migrations, Sanity Functions, and framework integrations including Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, and Hydrogen. Use this skill when setting up Sanity projects, designing content models, writing queries, implementing live preview, or integrating Sanity with a frontend framework.

Generated from the current SKILL.md.

Does this skill cover framework integrations like Next.js, Nuxt, and Astro?
Yes. The skill includes integration guides for Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and standalone Studio patterns.
What does this skill cover for GROQ queries?
The skill provides GROQ query patterns, type safety approaches, and performance optimization guidelines, with reference materials for detailed examples.
Does this skill include guidance on Sanity Functions and event automation?
Yes. The skill covers Sanity Functions for automating content workflows, including documentEventHandler and defineDocumentFunction patterns.
Can I use this skill for schema design and content modeling?
Yes. The skill provides schema design best practices, field definitions, validation patterns, and content modeling guidance for different use cases.
Does this cover Visual Editing and live preview setup?
Yes. The skill includes guidance on the Presentation Tool, Stega, overlays, and live preview configuration for frontend frameworks.

Generated from the current SKILL.md. These answers refresh after source changes.