Table of contents
- Quick summary
- Find out which deployment is actually live
- Path A: Autoscale or Reserved VM, a route that builds itself
- Path B: Static Deployment, a file you rebuild
- Path A vs Path B, side by side
- Point `<head>` at whichever one you shipped
- Confirm it, in order
- Where AIScan fits, and where it doesn't
- Ship it, then check the row that changed
A Replit app can serve /feed.xml two completely different ways, and which one applies to you depends on a setting you may never have opened. Get it backwards and you'll ship a feed that 404s in production while it worked fine in the workspace preview.
Quick summary
| Check | E5 (Content feed: RSS, Atom or JSON Feed) |
| Platform | Replit |
| The fork | Autoscale and Reserved VM run your Express server; Static Deployments serve files only, no backend |
| Fix | A live route on Autoscale/Reserved VM, or a rebuilt static file on Static |
| Time | About 10 minutes once you know which deployment type is live |
| Verify | curl the feed, grep the <head> declaration, then run the AIScan scan |
Find out which deployment is actually live
Replit publishes an app one of four ways: Autoscale, Static, Reserved VM, or Scheduled, chosen from the Deployment type dropdown under Adjust settings in the Publishing tool. Autoscale is the default, and it's what you get automatically from an Agent-built app: according to Replit's own deployment-types documentation, verified on 28 September 2026, "Static Deployments are not compatible with Replit Apps created using Agent." The reason is the one that matters for a feed too: an Agent build ships a real backend, and that backend is exactly what Path A below needs. Scheduled deployments run a command and stop, with no public URL, so they're out of scope for a feed entirely.
The response headers, fetched from your own domain, tell you which one is live without opening the dashboard:
curl -sI https://yourapp.replit.app/ | grep -i '^server\|^x-powered-by'
| Response | Deployment | Backend? |
|---|---|---|
x-powered-by: Express | Autoscale or Reserved VM | Yes, your server answers every route |
server: nginx, no x-powered-by | Static | No, only files in the public directory are served |
This is the same header signature the complete Replit setup guide uses to tell the two apart before touching anything else on the site, verified again for this post. It also decides how the fixes in the title, meta and schema guide get applied, so it's worth confirming once and remembering.
Path A: Autoscale or Reserved VM, a route that builds itself
If your server is answering, add a route next to your other routes that assembles the feed from whatever already holds your content (a database table, a CMS call, a folder of Markdown files) and serve it with the right content type:
app.get('/feed.xml', async (req, res) => {
const items = await getRecentPosts(20); // newest first, real dates
res.set('Content-Type', 'application/rss+xml; charset=utf-8');
res.send(buildRssXml(items));
});
Register it before any catch-all or SPA fallback route. A wildcard app.get('*', ...) placed above /feed.xml will swallow the request and hand back your app shell instead of XML, and the response will still be a 200, which is exactly the kind of failure a status-code check misses. Because this runs on every request, the feed is never stale: publish a post, and the next fetch reflects it with no rebuild step.
Path B: Static Deployment, a file you rebuild
A Static Deployment has no server to answer a request live, so feed.xml has to exist as an actual file in the folder you're publishing. Two things follow: it has to be produced by your build (or committed by hand), and it goes stale the moment you publish new content until you rebuild and republish.
Adjust settings > Deployment type > Static, then set the public directory to your build's output folder (dist for a typical Vite project). If your static site generator can emit a feed as part of its normal build, most can, from a template or a plugin, point that output at the same public directory so feed.xml ships automatically every time you republish. If it can't, write the file into the public directory before each publish and remember to redo it. A Static Deployment that shipped a feed once and never rebuilt it again is worse for E5 than shipping no feed at all, because the dates inside it start lying about what's fresh.
One more static-only detail: if curl later shows the wrong Content-Type for the file, you can force it. According to Replit's Static Deployment configuration page, fetched from docs.replit.com on 28 September 2026, Content-Type isn't on the list of reserved response headers, so a [[deployment.responseHeaders]] entry in your .replit file scoped to path = "/feed.xml" overrides it on your next republish.
Path A vs Path B, side by side
| Autoscale / Reserved VM | Static Deployment | |
|---|---|---|
| Where the feed lives | Generated in a route handler | A real file in the public directory |
| Freshness | Live on every request | As fresh as your last rebuild |
| What breaks it | A catch-all route registered above it | A build that doesn't emit or copy the file |
Point <head> at whichever one you shipped
Every page that should surface the feed needs the same declaration, adjusted for your format:
<link rel="alternate" type="application/rss+xml" title="Your Site" href="/feed.xml" />
Use application/atom+xml for Atom or application/feed+json for JSON Feed. Match the type to what you actually built, not to whichever example you copied. On Autoscale, this goes in whatever template renders your page <head> on every route, so it doesn't depend on which page the crawler lands on first. On Static, there usually isn't a template: you're editing the built index.html (or your generator's layout partial) directly, and it's worth checking that every page, not just the homepage, inherited the change.
Confirm it, in order
curl -sI https://yourapp.replit.app/feed.xml # expect 200 + a feed content-type
curl -s https://yourapp.replit.app/feed.xml | head -20 # real items, real dates
curl -s https://yourapp.replit.app/ | grep -o '<link[^>]*alternate[^>]*>'
If the first command 404s on Autoscale, the route is either missing or sits below a catch-all: move it. If it 404s on Static, the file isn't in the public directory the deployment is actually serving; confirm the directory under Adjust settings, not the one you assumed. If the <link> grep comes back empty, the feed exists but nothing points a crawler at it, which is worth catching because it's invisible in a browser. The feed works if you paste its URL directly, and still fails the check.
Then dogfood it the way this check is actually graded: lead with the scan, not the curl. Run npx aiscan-cli yourapp.replit.app naming check E5, or paste the URL at aiscan.site. The manual commands above are the by-hand equivalent if you'd rather not run anything.
Where AIScan fits, and where it doesn't
E5 confirms the feed exists, resolves, parses as valid RSS, Atom, or JSON Feed, and is declared in <head> where a crawler expects to find it, the same four things you just verified. AIScan's own guidance for this check, fetched from its check-guidance API this run, cites RFC 4287 for Atom and JSON Feed 1.1 as the two specs a valid feed has to satisfy. What none of that confirms is whether the dates inside stay honest over time. A feed that was accurate on launch day and never rebuilt again on a Static Deployment will keep passing the check while quietly telling every agent that reads it that nothing has changed in months. That's a workflow problem, not a markup one, and no scanner catches it from a single request.
Ship it, then check the row that changed
A feed is the cheapest changed-since-last-week signal your Replit app can publish, and which of the two paths above applies is decided the moment you pick a deployment type, not something you can retrofit blind. Confirm the fix landed with a fresh AIScan scan, then look at what else check E5's neighbors in the content dimension are asking for; a valid feed rarely ships alone. If your app doesn't have a valid llms.txt yet, the Replit llms.txt guide walks the same deployment-type fork verified here. A Vite/React app on Lovable hits the identical live-route-vs-static-file choice, covered in the Lovable version of this fix. More Replit-specific fixes are collected under /docs/platforms/replit, and the full check list is at /guides.
Frequently asked questions
What does AIScan's E5 check actually look for on a Replit app?
It confirms a feed exists at a reachable URL, parses as valid RSS, Atom, or JSON Feed, and is declared with a <link rel="alternate"> tag in <head> so a crawler that never executes JavaScript can still find it. It does not care which of the three formats you pick, only that one of them is valid and declared.
I added the Express route but /feed.xml still returns a 404. What's wrong?
On Autoscale or Reserved VM, this is almost always route order. A catch-all or SPA fallback route registered above /feed.xml intercepts the request first and returns your app shell (usually with a 200, not a 404, which makes it easy to miss). Move the feed route above any app.get('*', ...) handler and redeploy.
curl shows my feed correctly, but the AIScan scan still fails E5. Why?
The feed passing on its own is only half of what E5 checks. If the <link rel="alternate"> declaration is missing from your page's <head>, or its type attribute doesn't match the format you actually built, a crawler has no way to discover the feed from the page itself, and the check fails even though the file is fine.
Do I need both a live route and a static file?
No. Build whichever one matches your deployment type. Check the response headers first (server: nginx with no x-powered-by means Static; x-powered-by: Express means Autoscale or Reserved VM) and only build that path. Shipping both is wasted work and one of them will always be stale.
My Static Deployment's next build wiped out the feed.xml I hand-edited. What happened?
A Static Deployment serves whatever your build process outputs into the public directory, and a fresh build overwrites anything that wasn't generated by it. Editing the deployed file by hand only survives until the next publish. Generate the feed as part of your build (from a template or plugin) so it's recreated every time, or accept that a hand edit needs to be redone before every republish.
Which deployment type does an Agent-built Replit app use by default?
Autoscale. Agent builds full-stack apps that need a backend server, so Static, which serves files with no backend at all, isn't offered as an option for that kind of app.
Does the feed need to list every post I've ever published?
No, and it shouldn't. Cap it at your 15-20 most recent items with real, accurate publish dates. A feed's job is to answer "what changed recently," not to serve as a full archive; a sitemap already covers everything that exists.
Can I ship a JSON Feed instead of RSS or Atom?
Yes, AIScan's E5 check accepts any of the three. Just make sure the <link> tag's type attribute matches what you built: application/feed+json for JSON Feed, application/rss+xml for RSS, application/atom+xml for Atom. A mismatched type is functionally the same failure as no declaration at all.
Related guides
How to publish an RSS/Atom/JSON feed and declare it in the head on Docusaurus
Docusaurus is the one platform in this series where the feed usually exists before you ask for it. @docusaurus/presetclassic includes the blog plugin, and the blog plugin writes RSS and Atom files on…
How to publish an RSS/Atom/JSON feed and declare it in the head on Lovable
A sitemap tells a crawler what exists. It does not tell an agent what changed since last week, and that gap is what check E5 grades: whether the site publishes a dated content feed at all. Run…
How to ship one h1, title, meta description and JSON-LD on Replit
| Question | Answer | ||| | Where does Replit check this? | The SEO Rating card in the Growth pane, calculated after every publish | | Does that rating cover all four C3 signals? | No. Title, meta…
Add an RSS Feed AI Agents Can Actually Find
Two staticsite generators build you a feed with zero configuration. One declares it in <head on every site we tested. The other only manages it on five of every eight. Same defaulton feature, same…
