Textless dark-green cover with concentric mint and coral arcs on the right for a live feed route, a faint file grid in the corner for a static file, and negative space on the left for the headline.
Textless dark-green cover with concentric mint and coral arcs on the right for a live feed route, a faint file grid in the corner for a static file, and negative space on the left for the headline.
AI Readiness

How to publish an RSS/Atom/JSON feed and declare it in the head on Replit

Replit's Autoscale and Static deployments store an RSS feed differently. Add a live route or a static file, declare it in head, and verify with AIScan check E5.

AAsif Rahman 28 Sept 2026 7 min read
#Replit#RSS#Atom#JSON Feed#content signals#Autoscale#Static Deployment

This guide covers E5 · Content — for Replit.

Table of contents

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

CheckE5 (Content feed: RSS, Atom or JSON Feed)
PlatformReplit
The forkAutoscale and Reserved VM run your Express server; Static Deployments serve files only, no backend
FixA live route on Autoscale/Reserved VM, or a rebuilt static file on Static
TimeAbout 10 minutes once you know which deployment type is live
Verifycurl 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'
ResponseDeploymentBackend?
x-powered-by: ExpressAutoscale or Reserved VMYes, your server answers every route
server: nginx, no x-powered-byStaticNo, 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 VMStatic Deployment
Where the feed livesGenerated in a route handlerA real file in the public directory
FreshnessLive on every requestAs fresh as your last rebuild
What breaks itA catch-all route registered above itA 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