Skill 05 · Sanity Best Practices
Subchapter 5.14
references/nuxt.mdMarkdown6 KBView on GitHub
npm create nuxt@latest my-app -- -t ui -M "" --packageManager npm --no-gitInit
cd my-app-t ui selects the Nuxt UI starter. -M "" skips the interactive module-selection prompt (empty string = no extra modules). --packageManager npm and --no-gitInit suppress the other two prompts so the scaffold runs end-to-end without input.
npx nuxi@latest module add sanitynuxi module add sanity resolves to the official @nuxtjs/sanity module and registers it in nuxt.config.ts automatically. The module bundles @sanity/client, @sanity/visual-editing, @portabletext/vue, and groq as direct dependencies — no separate installs needed.
groq and defineQuery are also auto-imported by the module, so you can use them in .vue files without an import statement.
For manual image-URL building (an alternative to the auto-registered <SanityImage> component), add @sanity/image-url:
npm install @sanity/image-urlComposables (use directly in <script setup>, no imports needed):
useSanity() — get the client and its configuseSanityQuery() / useLazySanityQuery() — reactive query helpersuseSanityConfig() — read the resolved module configuseSanityPerspective(), useSanityPreviewPerspective(), useSanityPreviewEnvironment() — perspective helpers for drafts/previewuseSanityVisualEditingState(), useIsSanityLivePreview(), useIsSanityPresentationTool() — visual-editing state helpersGROQ helpers (template tags): groq, defineQuery
Components (use directly in <template>):
<SanityContent> — Portable Text renderer (uses @portabletext/vue internally; prop is :value)<SanityImage> — image renderer; takes an assetId (the image asset’s _ref); upgrades to <NuxtImg> automatically when @nuxt/image is installed<SanityFile> — file rendererexport default defineNuxtConfig({
modules: ['@nuxtjs/sanity'],
sanity: {
projectId: process.env.NUXT_SANITY_PROJECT_ID,
dataset: process.env.NUXT_SANITY_DATASET,
apiVersion: '2026-05-15',
// Live Visual Editing Configuration
visualEditing: {
studioUrl: process.env.NUXT_SANITY_STUDIO_URL,
token: process.env.NUXT_SANITY_API_READ_TOKEN, // Required for fetching drafts
stega: true, // Enable stega for visual editing
mode: 'live-visual-editing', // Default: enables live updates
},
},
});Important: Don’t enable the minimal client if you want the full feature set (composables, components, visual editing).
Use the composable for reactive fetching. It handles preview state automatically when visualEditing is configured. groq and defineQuery are auto-imported — use either.
<!-- app/pages/posts.vue -->
<script setup lang="ts">
const query = groq`*[_type == "post" && defined(slug.current)]{ _id, title, slug }`
const { data: posts } = await useSanityQuery<Array<{ _id: string; title?: string; slug?: { current?: string } }>>(query)
</script>
<template>
<ul>
<li v-for="post in posts || []" :key="post._id">
<NuxtLink :to="`/${post.slug?.current}`">{{ post.title }}</NuxtLink>
</li>
</ul>
</template>Pull the slug off useRoute() and pass it as a query parameter. The <SanityContent> component renders Portable Text — note the prop is value, not blocks (renamed in v2).
<!-- app/pages/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const query = groq`*[_type == "post" && slug.current == $slug][0]{ _id, title, body }`
const { data: post } = await useSanityQuery<{ _id: string; title?: string; body?: unknown[] }>(
query,
{ slug: route.params.slug }
)
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<SanityContent v-if="post.body" :value="post.body" />
</article>
</template>When visualEditing is configured in nuxt.config.ts, the module handles:
If you use stega-encoded strings in logic (e.g. v-if="post.layout === 'full'"), you must clean them. stegaClean is exported from @sanity/client/stega (a transitive of @nuxtjs/sanity, so no separate install).
import { stegaClean } from '@sanity/client/stega'
const layout = computed(() => stegaClean(props.layout))The module auto-registers <SanityContent>. Don’t install @portabletext/vue separately; it’s a direct dep of the module.
<SanityContent :value="post.body" />For custom blocks/marks, pass :components:
<SanityContent :value="post.body" :components="{ block: { h2: MyH2 } }" />Option A — <SanityImage> (recommended). Auto-registered. Takes the asset’s _ref (the assetId) and builds the URL via the module’s resolved projectId/dataset. If @nuxt/image is installed, it transparently upgrades to <NuxtImg> for responsive sizing.
<SanityImage :asset-id="post.mainImage.asset._ref" width="800" />Option B — @sanity/image-url builder. Install @sanity/image-url separately and build URLs manually. Useful when you need fine-grained control (hotspot/crop, format negotiation, srcset).
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder(useSanity().client)
// builder.image(source).width(800).url()