
Short answer: Wagtail does not ship an RSS feed, but it runs on Django, so the cleanest way to build a Wagtail RSS feed is Django’s syndication framework. Write a Feed class that returns your live, public blog pages ordered by publish date, use each page’s full_url as the link, add the route before Wagtail’s catch-all URL pattern, and make every link and image absolute. Once the feed validates, connect it to an auto-poster so each newly published page is shared on social media automatically.
Wagtail is a popular Django-based CMS used by newsrooms, universities, charities and product teams. Editors love its page tree and StreamField, and developers like that everything is plain Django underneath. That last point is what makes feeds simple: you do not need a Wagtail-specific plugin, because Django already has a well-tested feed generator. This guide shows how to wire it to Wagtail page models, how to handle rich text, images and multiple sites, how to keep the feed fresh behind a cache, and what to check before an automation tool starts reading it.
No. A fresh Wagtail project has pages, images, documents and search, but no feed URL. That is a deliberate choice: Wagtail does not know which of your page types are articles, which date field matters or how much text you want to publish. You decide that in code.
There are three common approaches:
Feed class with its own URL, such as /blog/feed/. Simple, explicit and easy to test. This is the approach used in most of this guide.RoutablePageMixin, so every blog index in the page tree gets its own feed automatically.Whichever you choose, the output is a normal RSS 2.0 or Atom document that any reader or automation tool can use.
The examples assume a typical blog structure: a BlogIndexPage with BlogPage children. A minimal article model looks like this:
from django.db import models
from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel
class BlogPage(Page):
intro = models.CharField(max_length=300)
body = RichTextField()
cover = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+')
content_panels = Page.content_panels + [
FieldPanel('intro'),
FieldPanel('cover'),
FieldPanel('body'),
]Two fields do most of the work in the feed. The intro field becomes the item description, so ask editors to write it as a real summary of one or two sentences. Wagtail’s own first_published_at field, which every page has, becomes the publication date. It is set automatically the first time a page goes live and does not change on later edits, which is exactly what a feed needs. In older projects the imports live under wagtail.core; the logic is the same.
Create blog/feeds.py:
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Rss201rev2Feed
from wagtail.models import Site
from .models import BlogPage
class BlogFeed(Feed):
feed_type = Rss201rev2Feed
title = 'Example Blog'
description = 'News and articles from Example'
def __call__(self, request, *args, **kwargs):
self.request = request
self.site = Site.find_for_request(request)
return super().__call__(request, *args, **kwargs)
def link(self):
return self.site.root_url + '/blog/'
def items(self):
return (BlogPage.objects.live().public()
.in_site(self.site)
.order_by('-first_published_at')[:20])
def item_title(self, item):
return item.title
def item_description(self, item):
return item.intro
def item_link(self, item):
return item.full_url
def item_pubdate(self, item):
return item.first_published_at
def item_updateddate(self, item):
return item.last_published_atA few details are worth understanding rather than copying:
live() و public() exclude drafts, unpublished pages and pages behind a view restriction such as a password or login. Without them, a private page can leak into the feed and from there onto social media.in_site() keeps the feed limited to the site the request came to, which matters as soon as one Wagtail installation serves several domains.full_url returns the absolute address of the page including scheme and domain. Feed readers and auto-posters see links out of context, so relative paths do not work for them.Note that Django’s feed view uses the separate django.contrib.sites framework, or the request host when that app is not installed, to complete relative links. Wagtail has its own Site model, and the two are not connected. Returning absolute URLs yourself, as above, avoids any confusion between them.
Wagtail serves pages through a catch-all URL pattern, so the feed route has to come first in your project’s urls.py:
from django.urls import path, include
from wagtail import urls as wagtail_urls
from blog.feeds import BlogFeed
urlpatterns = [
# admin, documents and other routes ...
path('blog/feed/', BlogFeed(), name='blog_feed'),
path('', include(wagtail_urls)),
]If the feed line comes after the Wagtail include, Wagtail tries to find a page called feed under your blog and returns a 404. This ordering mistake is the most common reason a new Wagtail feed does not work.
Pick the address once and keep it. Every reader, aggregator and automation tool that subscribes stores the URL, and changing it later means updating all of them. Advice on choosing it is in which RSS feed URL to use.
Larger sites often have several sections, such as news, events and research, each with its own index page. Instead of writing a URL for each, make the index page routable:
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class BlogIndexPage(RoutablePageMixin, Page):
@route(r'^feed/$')
def feed(self, request):
return SectionFeed(self)(request)إضافة wagtail.contrib.routable_page to INSTALLED_APPS, and give SectionFeed a constructor that stores the index page so its items() method can return BlogPage.objects.live().public().child_of(self.index). Every index page now answers at its-url/feed/, so /news/feed/ و /research/feed/ exist without extra configuration.
Section feeds are useful well beyond readers. You can send research updates to LinkedIn, event announcements to a Telegram channel and everything to Mastodon, each from its own feed. The idea is explained in category filtering for auto-posting.
A summary from the intro field is enough for most feeds and works best for social posts, where only the first sentences are visible. If you also want full content for feed readers, there are two traps.
First, Wagtail stores rich text in an internal format in which links to pages and documents are references rather than URLs. Always convert it before output:
from wagtail.rich_text import expand_db_html
def item_description(self, item):
return expand_db_html(item.body)Second, the converted HTML may still contain site-relative links and image addresses, such as /media/images/photo.width-800.jpg. Inside a feed they break. Either rewrite them to absolute URLs with a small helper that prefixes self.site.root_url, or serve media from a storage backend or CDN that already returns absolute URLs.
For StreamField bodies, render the blocks to HTML with the field’s own rendering and apply the same URL fix. Keep embeds in mind: video embeds and custom blocks may produce markup that feed readers strip, so test with a real reader. When you need both, a common pattern is a summary feed for automation and a separate full-text feed for readers. The trade-offs are covered in full-text vs excerpt RSS feeds.
Social posts with a good picture get far more attention than plain links. Auto-posting tools look for an image in the feed item and in the Open Graph tags of the linked page, so give them both.
In the feed, an enclosure is the most widely understood way to attach an image. Django supports it through three item methods:
def item_enclosure_url(self, item):
if item.cover:
r = item.cover.get_rendition('fill-1200x630')
return self.request.build_absolute_uri(r.url)
def item_enclosure_mime_type(self, item):
return 'image/jpeg'
def item_enclosure_length(self, item):
return 0build_absolute_uri turns a relative rendition URL into an absolute one and leaves an absolute CDN URL unchanged. Use the real file size if you have it, and set the MIME type to match the rendition format. Then make sure your page template outputs an og:image meta tag with the same rendition, so link previews show the same picture. More on choosing the image is in how to control which image gets auto-posted.
Django can produce either format from the same class. Switch the output by changing one line:
from django.utils.feedgenerator import Atom1Feed
class BlogAtomFeed(BlogFeed):
feed_type = Atom1Feed
subtitle = BlogFeed.descriptionAtom uses subtitle instead of description and makes good use of the updated date. Most automation tools read both formats. If you are unsure which one to offer, offer RSS 2.0 and add Atom only if someone asks for it; the differences are summarised in Atom vs RSS for auto-posting.
| Feed element | Wagtail source | Why it matters |
|---|---|---|
| link | page.full_url | Absolute URL that works outside the site |
| pubDate | first_published_at | Stable date that does not change on edits |
| guid | Defaults to the link | Keeps items unique for readers and tools |
| description | intro or expanded rich text | The text shown in readers and posts |
| enclosure | Image rendition | The picture used in social posts |
| Item filter | live(), public(), in_site() | Keeps drafts and private pages out |
Wagtail lets editors schedule pages with a go-live date. A scheduled page is not live until the publish_scheduled management command runs (named publish_scheduled_pages in older releases), so run it from cron every few minutes. Until then the page is not in the feed, which is what you want: anything that appears in the feed may be shared immediately.
Edits are a separate matter. The GUID that Django writes defaults to the item link, so changing a page’s slug after publication changes its GUID, and automation tools may treat the edited page as a new post. Decide slugs before publishing, or define item_guid to return something stable such as the page ID combined with your domain. Why this matters is explained in why a feed auto-posts the same article twice.
Caching is the last piece. If you use Wagtail’s frontend cache invalidation with a CDN or reverse proxy, it purges a page’s URLs when the page is published, but the feed lives at a different URL. Either give the feed a short cache time, or override get_cached_paths() on the index page so the feed path is purged as well. Django’s per-view cache can also wrap the feed view, but keep its timeout short.
Add an autodiscovery link to the head of your base template so browsers, readers and tools can find the feed from any page:
<link rel="alternate" type="application/rss+xml"
title="Example Blog" href="/blog/feed/">Then validate. Open the feed in a browser, run it through the W3C Feed Validation Service, and check the details a validator does not judge:
link and image URL starts with your real https domain.application/rss+xml; Django sets this for you, but a proxy can change it.The framework’s options are documented in the Django syndication feed framework reference, and Wagtail’s page query methods in the Wagtail documentation. A quick way to inspect headers and status codes is described in debugging an RSS feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your Wagtail feed, or of each section feed, connect your accounts and choose how posts are built from the title, description or page title. PostRSS checks each feed as often as every minute and shares new pages, with their image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, Mastodon, Bluesky, Telegram, Discord and DEV. Keyword filters, UTM parameters, hashtags from categories and posting windows let you adjust each channel. Everything it does is listed on the PostRSS features page, and plan limits are on the pricing page. More developer guides are under the Technical Guide tag.
A Wagtail RSS feed is a small Django Feed class: return live, public pages from the current site, newest first, use full_url for links and first_published_at for dates, and register the route before Wagtail’s catch-all pattern. Add a cover image as an enclosure, make every URL absolute, keep the GUID stable and the cache short. Validate it once, add an autodiscovery link, and every page you publish in Wagtail can reach your social channels without anyone copying links by hand.
No. Wagtail has no feed out of the box, but because it is built on Django you can create one with Django’s syndication framework in a few dozen lines of code.
The feed route is probably listed after the Wagtail catch-all pattern in urls.py. Move it above the include of Wagtail’s URLs so Django matches it first.
Filter the queryset with live() and public(). Live excludes drafts and unpublished pages, and public excludes pages with a password, login or group restriction.
Yes. Add RoutablePageMixin to the index page model and define a feed route, so every index page answers at its own feed address with only its child pages.
Image renditions and rich text often use site-relative URLs. Convert them to absolute URLs with build_absolute_uri or the site root URL before they go into the feed.
What changed in the networks, what broke, and how to fix it before it costs you reach.
من تطوير Internet Solutions، الفريق الذي يقف وراء PostRSS. كل منتج يوفّر وقتك بطريقته الخاصة.