RSS a 66 redes sociales: Facebook, Instagram, X, LinkedIn, Telegram y más Blog Afiliados Contactos
Iniciar sesión Empezar gratis
Updated: 2026-10-11
Sanity RSS Feed: Query Posts with GROQ and Auto-Post Them

Short answer: Sanity has no built-in RSS feed, because it is a headless content platform that serves structured content through an API. To build a Sanity RSS feed, write a GROQ query that returns your latest published posts with their slug, date, summary and image URL, run it from a server route in your front end, for example a Next.js route handler at /rss.xml, and render the result as RSS XML. Use your website’s URLs for links, Sanity’s CDN URLs for images, the document ID as a stable GUID, and refresh the feed with a webhook. Then connect the feed to an auto-poster so every new post is shared on social media automatically.

Sanity is popular with product teams and publishers because content is stored as structured JSON in its Content Lake, edited in a customisable Studio and queried with GROQ, a compact query language. The website is a separate application, usually built with Next.js, Astro, Remix, SvelteKit or Nuxt. That separation is why a feed is not created for you: Sanity does not know your website’s URLs. This guide shows the schema fields a feed needs, the GROQ query, a complete route handler, how to handle Portable Text and images, how to keep drafts and scheduled posts out, and how to refresh the feed when editors publish.

Why a Sanity project has no feed yet

A feed needs four things: a list of items, a public URL for each one, a date and some text. Sanity has the first, third and fourth, but only your front end knows the second. A post with the slug autumn-update might live at /blog/autumn-update, /en/news/autumn-update or in a mobile app. So the feed belongs in the front end, which combines Sanity content with its own routing.

This is the same situation as with other headless platforms, described in general in auto-posting from a headless CMS without native RSS. The difference with Sanity is that GROQ lets you shape the data exactly as the feed needs it in a single query.

Schema fields the feed needs

Most Sanity blog schemas already have what is required. A typical post type in the Studio:

import { defineType, defineField } from 'sanity';

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
    defineField({ name: 'publishedAt', type: 'datetime' }),
    defineField({ name: 'excerpt', type: 'text', rows: 3 }),
    defineField({ name: 'mainImage', type: 'image' }),
    defineField({ name: 'body', type: 'array', of: [{ type: 'block' }] }),
  ],
});

Two fields deserve attention. publishedAt should be set by editors, or filled automatically with an initial value, because Sanity’s system timestamps _createdAt y _updatedAt do not mean the same thing: the first is when the document was created, often long before publication, and the second changes on every edit. The excerpt field gives you a clean summary of one or two sentences, which is exactly what a social post should start with. Add validation rules so neither can be left empty.

The GROQ query

One query returns everything the feed needs, already in the right shape:

*[_type == "post"
  && defined(slug.current)
  && defined(publishedAt)
  && publishedAt <= now()]
| order(publishedAt desc)[0...20]{
  _id,
  title,
  "slug": slug.current,
  publishedAt,
  "summary": coalesce(excerpt, pt::text(body)[0...300]),
  "image": mainImage.asset->url
}

Line by line:

  • defined(slug.current) skips posts without a slug, which would produce broken links.
  • publishedAt <= now() keeps posts with a future date out of the feed until that moment, which is useful when editors prepare posts in advance.
  • [0...20] limits the feed to the 20 newest posts. The three dots make the range exclusive, so this returns items 0 to 19.
  • pt::text(body) converts Portable Text to plain text, a fallback when the excerpt is empty.
  • mainImage.asset->url follows the reference to the image asset and returns its absolute CDN URL.

Keep drafts out of the feed

In Sanity, a draft is a separate document whose ID starts with drafts.. Drafts are only visible to authenticated requests, so a query made without a token from a public dataset returns published documents only. If your front end uses a token, for example for preview mode, make the feed request explicit:

const posts = await client.fetch(FEED_QUERY, {}, { perspective: 'published' });

Recent API versions use the published perspective by default, but setting it in the feed code costs nothing and protects you from a configuration change later. Test it once: create a draft with an obvious title and confirm it does not appear. Anything that reaches the feed can be shared by automation within minutes.

Build the route in Next.js

With the App Router, a feed is a route handler. Create app/rss.xml/route.ts:

import { createClient } from '@sanity/client';
import { Feed } from 'feed';

const SITE = process.env.SITE_URL ?? 'https://example.com';

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: 'production',
  apiVersion: '2025-02-19',
  useCdn: true,
});

export const revalidate = 600;

export async function GET() {
  const posts = await client.fetch(FEED_QUERY, {}, { perspective: 'published' });

  const feed = new Feed({
    title: 'Example Blog',
    description: 'News and articles from Example',
    id: SITE + '/blog',
    link: SITE + '/blog',
    copyright: 'Example',
  });

  for (const p of posts) {
    const url = SITE + '/blog/' + p.slug;
    feed.addItem({
      title: p.title,
      id: 'sanity:' + p._id,
      link: url,
      description: p.summary,
      date: new Date(p.publishedAt),
      image: p.image ? p.image + '?w=1200&h=630&fit=crop' : undefined,
    });
  }

  return new Response(feed.rss2(), {
    headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
  });
}

The feed package from npm handles XML escaping and date formatting. revalidate tells Next.js to regenerate the cached feed at most every ten minutes. The same code works in an Astro endpoint, a Remix or React Router resource route or a SvelteKit server route, with only the export shape changing. Framework details are covered in adding an RSS feed to a Next.js site y the Remix RSS feed guide.

Images from the Sanity CDN

Sanity serves images from its own CDN with absolute URLs, so the usual relative path problem does not arise. The CDN also transforms images through URL parameters: w y h set the size and fit=crop crops to fill. A 1200 by 630 crop matches the preview format most networks use. If you need hotspot and crop settings from the Studio respected, use the @sanity/image-url builder instead of adding parameters by hand:

import imageUrlBuilder from '@sanity/image-url';
const builder = imageUrlBuilder(client);
const img = builder.image(post.mainImage).width(1200).height(630).url();

For that, return the whole mainImage object from GROQ rather than only the URL. Use the same image for the og:image tag on the post page, because many networks build the link preview from the page itself. Image selection is explained in how to control which image gets auto-posted.

Portable Text and full-content feeds

Sanity stores rich text as Portable Text, an array of JSON blocks, not HTML. For a summary feed, the excerpt or pt::text() is all you need. If you want full content for feed readers, convert the blocks to HTML on the server:

import { toHTML } from '@portabletext/to-html';
const html = toHTML(post.body, { components: myComponents });

Custom block types, such as code blocks, embeds or call-to-action boxes, need their own serializers in components, or they are dropped. Internal links stored as references must be turned into absolute URLs, and images inside the body need CDN URLs. A common compromise is a summary feed for automation and a separate full-text feed for readers; the trade-offs are explained in full-text vs excerpt RSS feeds.

Feed elementSanity sourceNote
titletitlePlain string, escaped by the feed library
linkSite URL plus slug.currentBuilt in the front end
guid_idStable even if the slug changes
pubDatepublishedAtEditor-controlled, not _updatedAt
descriptionexcerpt or pt::text(body)Short plain text works best
imagemainImage.asset->urlAbsolute CDN URL, resizable

Refresh the feed when editors publish

With time-based revalidation, a new post appears in the feed within the revalidate window. For faster updates, create a GROQ-powered webhook in the Sanity project settings that fires when a post is created or updated, and point it at an API route in your app that calls revalidatePath('/rss.xml'). Protect that route with the webhook secret; the next-sanity package includes a helper to verify the signature.

Be aware of the API CDN as well. With useCdn: true, queries are served from a cache that is usually updated quickly but not instantly. If the webhook triggers a rebuild a moment after publishing, a CDN response can still miss the newest post. For the feed route, consider useCdn: false, which always reads fresh data and is fine for a request that runs only every few minutes. How automation tools detect new items is described in RSS vs webhooks vs API polling.

Category and language feeds

Because the feed is just a query, topic feeds cost almost nothing. Add a dynamic route such as app/rss/[category]/route.ts and pass the category to GROQ as a parameter, so user input is never pasted into the query string:

*[_type == "post"
  && $category in categories[]->slug.current
  && publishedAt <= now()]
| order(publishedAt desc)[0...20]{ _id, title, "slug": slug.current, publishedAt, excerpt }

Call it with client.fetch(QUERY, { category: params.category }). A product-updates feed can then go to LinkedIn and X, while a tutorials feed goes to DEV and Mastodon. Multilingual projects work the same way: whether you store languages as separate documents with a language field or with a translation plugin, filter by language and publish one feed per locale, connected to the accounts for that audience. Topic routing is explained in category filtering for auto-posting.

Test before connecting automation

  1. Open /rss.xml in a private window and check that the newest post is first.
  2. Confirm the content type is application/rss+xml and the status is 200.
  3. Click several item links. They must open on your public domain, not on the Studio.
  4. Open the image URLs and check the crop.
  5. Run the feed through the W3C Feed Validation Service.

GROQ functions, perspectives, the image pipeline and webhooks are documented in the Sanity documentation.

How PostRSS works with a Sanity site

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the item title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new post, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Threads, Bluesky, Mastodon and Slack through webhooks. Keyword filters, UTM parameters, hashtags from categories and posting windows let you adjust each channel, and the REST API is available for your own integrations. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

Related reading

The bottom line

A Sanity RSS feed is a GROQ query plus a server route in your front end. Select published posts with a slug and a past publication date, newest first, return a plain summary and the image URL, build links from your own site and use the document ID as the GUID. Render with a feed library, revalidate on a short timer or through a webhook, and validate the result. Then every post your editors publish in Sanity can reach your social channels automatically.

FAQ

Does Sanity have a built-in RSS feed?

No. Sanity is a headless content platform that serves JSON through its API. You build the feed in your front end with a GROQ query and a server route that returns XML.

How do I keep Sanity drafts out of my RSS feed?

Query with the published perspective, or without a token on a public dataset. Drafts have IDs that start with drafts and are only visible to authenticated requests.

Which date should a Sanity feed use?

Use an editor-controlled publishedAt field. The system field _updatedAt changes on every edit and _createdAt marks creation, so neither is a reliable publication date.

Are Sanity image URLs absolute?

Yes. Image assets are served from the Sanity CDN with absolute URLs, and you can add width, height and crop parameters to get a size that suits social previews.

How can my Sanity feed update right after publishing?

Create a webhook in the Sanity project that calls a protected route in your app, and revalidate the feed path there. Reading the feed data without the API CDN avoids stale results.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Más herramientas de nuestro equipo

Creadas por Internet Solutions, el equipo detrás de PostRSS. Cada una te ahorra tiempo a su manera.