Next.js Adds Revalidation, But Doesn't Allow Sub-Queries in Webhooks?

2 replies
Last updated: Sep 5, 2022
NextJS added revalidation, which is amazing because you can re-build specific pages when a sanity webhook is triggered, HOWEVER, Sanity doesn't allow sub-queries in webhooks. So this:

{
  "slug":slug,
  "type":_type,
  "revalidate_relevant_items":true,
  "related_programs": *[_type == "programma"]
}
doesn't work. When one post gets updated, I want all other posts that reference that post to get updated as well, does anyone have a workaround for this? For example: I have a 'theme' document type. Then I have lots of 'blog' documents that reference different themes. When I edit the title of a theme, all these blog posts need to get revalidated in NextJS. NextJS doesn't have the option to do dynamic revalidation yet, so I was hoping that Sanity could send the relevant blog post urls to the revalidation API, but apparently that doesn't work?

In other words: is there a way to do sub-queries/groq queries in a webhook?
AI Update

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.

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:

  1. Webhooks handle the trigger: They're reliable for detecting content changes
  2. Your API route has full GROQ access: No projection limitations - you can query anything with the full power of GROQ, including the references() function
  3. You control the revalidation logic: Can handle complex scenarios like bidirectional references
  4. 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.

A bit more work on your end, but can the endpoint receiving the Sanity webhook do the query for all relevant documents (
related_programs
in this case) that need to be revalidated, then feed those to the revalidate function in Next?
yeah that's certainly possible, thanks for the suggestion! I was hoping I could do it without any extra api calls, but I think this is the only way at the moment.

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.

Was this answer helpful?