Sanity Integration
Sanity is a headless CMS where the content model lives in code and editors compose pages in a real-time Studio. Paired with Composable Frontends, Sanity owns the editorial layout and Shopware owns commerce - the two never duplicate each other.
Runnable example
A complete, working Nuxt example lives in examples/sanity-cms. This guide walks through how it is built. The Sanity Studio it reads from is a standalone project you create separately - see The Studio.
The pattern: content + commerce
The single rule that makes this work: store a reference, never a copy. Sanity keeps editorial content and a product's id; Shopware provides the live data.
| Concern | Owner | Why |
|---|---|---|
| Page layout, sections, copy, images, which products to feature | Sanity | editorial, versioned, editor-controlled |
| Product price, name, stock, availability, media | Shopware | live commerce data - changes constantly |
| Cart, totals, checkout, logged-in user | Shopware | transactional, per-user, real-time |
Sanity (page.pageBuilder[]) --GROQ--> Nuxt --productIds--> Shopware Store API --> live cards1. Install & configure
Add the official @nuxtjs/sanity module. It bundles @sanity/client, @portabletext/vue and groq, and auto-imports useSanityQuery, groq, and the <SanityContent> / <SanityImage> components.
npx nuxi@latest module add sanity// nuxt.config.ts
export default defineNuxtConfig({
extends: ["@shopware/composables/nuxt-layer"],
modules: ["@shopware/nuxt-module", "@nuxtjs/sanity"],
shopware: {
endpoint: "https://demo-frontends.shopware.store/store-api/",
accessToken: "<your-sales-channel-access-token>",
},
sanity: {
projectId: "<your-project-id>",
dataset: "production",
apiVersion: "2026-05-15",
useCdn: true, // public, cacheable reads
},
});A public dataset needs no token for the frontend to read. The Shopware accessToken is your sales-channel key.
2. Model content as a Page Builder
In the Studio, a page document holds an ordered array of section blocks the editor arranges freely. The featuredProducts block stores only Shopware product IDs:
// studio/schemaTypes/objects/featuredProducts.ts
import { defineField, defineType } from "sanity";
export const featuredProducts = defineType({
name: "featuredProducts",
title: "Featured products",
type: "object",
fields: [
defineField({ name: "heading", type: "string" }),
defineField({
name: "productIds",
title: "Shopware product IDs",
type: "array",
of: [{ type: "string" }],
}),
],
});// studio/schemaTypes/documents/page.ts
defineField({
name: "pageBuilder",
type: "array",
of: [
{ type: "hero" },
{ type: "featuredProducts" },
{ type: "richText" },
{ type: "banner" },
{ type: "gallery" },
],
});3. Render the page
Fetch the page builder with GROQ and map each block _type to a component. groq and useSanityQuery are auto-imported.
<!-- app/app.vue -->
<script setup lang="ts">
const PAGE_QUERY = groq`*[_type == "page"] | order(_createdAt asc)[0]{
title,
pageBuilder[]{ ... }
}`;
const { data: page } = await useSanityQuery(PAGE_QUERY);
</script>
<template>
<PageBuilder :sections="page?.pageBuilder ?? []" />
</template><!-- app/components/PageBuilder.vue -->
<script setup lang="ts">
import SectionHero from "./sections/SectionHero.vue";
import SectionFeaturedProducts from "./sections/SectionFeaturedProducts.vue";
// ...
const components = {
hero: SectionHero,
featuredProducts: SectionFeaturedProducts,
// richText, banner, gallery...
};
defineProps<{ sections: Array<{ _key: string; _type: string }> }>();
</script>
<template>
<component
:is="components[section._type]"
v-for="section in sections"
:key="section._key"
:section="section"
/>
</template>Rich text uses the module's <SanityContent :value="block.content" />, images use <SanityImage :asset-id="image.asset._ref" />.
4. Resolve products from Shopware
The featuredProducts block arrives with only IDs. Resolve them to live products with useProductSearch during SSR, so the cards render in the initial HTML:
<!-- app/components/sections/SectionFeaturedProducts.vue -->
<script setup lang="ts">
const props = defineProps<{
section: { _key?: string; heading?: string; productIds?: string[] };
}>();
const { search } = useProductSearch();
const { data: products } = await useAsyncData(
`featured-products-${props.section._key}`,
async () => {
const ids = props.section.productIds ?? [];
const resolved = await Promise.all(
ids.map((id) => search(id).then((r) => r.product).catch(() => null)),
);
return resolved.filter(Boolean);
},
);
</script>Match the sales channel
Product IDs are per sales channel. IDs from one channel return 404 in another - make sure the IDs stored in Sanity belong to the sales channel your accessToken points to.
5. Cart & notifications
Commerce interactions stay with Shopware composables. The product card adds to the cart and raises a toast; a mini cart reads the live cart:
const { addToCart } = useAddToCart(product);
const { pushSuccess } = useNotifications();
const add = async () => {
await addToCart();
pushSuccess(`${product.value.translated?.name} added to cart`);
};// the cart is per-user session state - load it on the client, not in cached SSR
const { cartItems, count, totalPrice, isEmpty, removeItem, refreshCart } = useCart();
onMounted(() => refreshCart());The Studio (the editor)
The Studio - where editors model content and compose pages - is a standalone Sanity project, separate from the Nuxt app. Scaffold one with npm create sanity@latest, add the page document and the block schemas shown above, then run it locally or deploy it to Sanity's hosting:
npx sanity dev # http://localhost:3333
npx sanity deploy # https://<name>.sanity.studioSee Sanity's Studio documentation for creating, configuring and deploying a Studio.