Next.js Adds Revalidation, But Doesn't Allow Sub-Queries in Webhooks?
You're absolutely right that webhooks in Sanity don't support subqueries in their GROQ projections - you can't use *[...] inside webhook payloads. This is a documented limitation that affects exactly the use case you're describing.
The good news is there's a solid workaround: have your Next.js API route do the GROQ query when it receives the webhook. This is the standard production-ready approach that many Sanity users rely on.
The Recommended Workaround
Here's how to handle this:
1. Set up a simple webhook that sends just the basic document info (no subqueries needed):
In your Sanity project settings, create a webhook with a projection like:
{
"_id": _id,
"_type": _type,
"slug": slug.current
}2. Create a Next.js API route that receives the webhook and does the heavy lifting:
// app/api/revalidate-webhook/route.js (App Router)
import { createClient } from '@sanity/client';
import { revalidatePath } from 'next/cache';
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: false,
token: process.env.SANITY_API_TOKEN, // Read token
});
export async function POST(request) {
const { _id, _type, slug } = await request.json();
// For your theme example:
if (_type === 'theme') {
// Revalidate the theme page itself
if (slug) {
revalidatePath(`/themes/${slug}`);
}
// NOW you can do the subquery to find related blogs
const relatedBlogs = await client.fetch(
`*[_type == "blog" && references($themeId)]{ "slug": slug.current }`,
{ themeId: _id }
);
// Revalidate each related blog post
for (const blog of relatedBlogs) {
revalidatePath(`/blog/${blog.slug}`);
}
console.log(`Revalidated ${relatedBlogs.length} blog posts referencing theme ${_id}`);
}
return Response.json({
revalidated: true,
count: relatedBlogs?.length || 0
});
}For Pages Router, use this instead:
// pages/api/revalidate-webhook.js
import { createClient } from '@sanity/client';
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: false,
token: process.env.SANITY_API_TOKEN,
});
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { _id, _type, slug } = req.body;
if (_type === 'theme') {
if (slug) {
await res.revalidate(`/themes/${slug}`);
}
const relatedBlogs = await client.fetch(
`*[_type == "blog" && references($themeId)]{ "slug": slug.current }`,
{ themeId: _id }
);
for (const blog of relatedBlogs) {
await res.revalidate(`/blog/${blog.slug}`);
}
}
return res.json({ revalidated: true });
}Why This Works
This approach solves your problem because:
- Webhooks handle the trigger: They're reliable for detecting content changes
- Your API route has full GROQ access: No projection limitations - you can query anything with the full power of GROQ, including the
references()function - You control the revalidation logic: Can handle complex scenarios like bidirectional references
- It's production-ready: This pattern is widely used and battle-tested
Additional Tips
Use revalidateTag for more flexibility: If you're using Next.js 13+ with cache tags, you can tag related content and revalidate by tag instead of individual paths:
// Tag your fetches when building pages
fetch(sanityQuery, { next: { tags: [`theme-${themeId}`] } })
// Then revalidate all content with that tag at once
revalidateTag(`theme-${themeId}`);Secure your webhook endpoint: Add validation to ensure requests are actually from Sanity using webhook signatures or a secret token.
Handle the references() function: The GROQ references() function is perfect for finding documents that reference your changed document - exactly what you need for the theme → blog relationship. It works in your API route queries even though it won't work in webhook projections.
This webhook + API route pattern is the established, production-ready solution that works reliably for Next.js on-demand revalidation scenarios with Sanity.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.