
Short answer: With Google Apps Script you can turn a Google Sheet into an RSS feed without a server. Write a doGet function that reads the rows of the sheet, escapes the text, builds the XML with absolute links and RFC 822 dates, and returns it with the RSS mime type. Deploy the script as a web app that anyone can open, copy the address, check it with a validator and give it to an auto-poster. Every row that you mark as published then becomes a post on your social networks.
Many small teams keep their content plan in a spreadsheet: a row for each article, a column for the title, one for the summary, one for the date and one for the status. A spreadsheet is easy for editors, and it needs no hosting. What it lacks is a feed, which is what auto-posters, readers and other tools expect. Google Apps Script closes the gap. It is a JavaScript environment that runs inside your Google account, can read a sheet and can publish a script as a small web address. This guide builds the feed step by step, explains the limits of the platform honestly and ends with a test routine.
Create a sheet with one row per post and fixed column headings in the first row. The script will depend on the order of the columns, so decide it once and keep it.
| Column | Content | Rule |
|---|---|---|
| A: slug | The permanent part of the address | Lowercase words with hyphens, never edited after publishing |
| B: title | The headline of the post | Plain text |
| C: summary | One or two complete sentences | No half sentences |
| D: published | Date and time of publication | A real date cell, not text |
| E: status | The word Published for live rows | Anything else keeps the row out |
Format column D as a date with a time, because two posts on the same day need a clear order. The status column is a simple safety switch: an editor can prepare a row in advance and set it to Published only when the page on the website is live. That prevents the most common problem with spreadsheet feeds, which is an item that points to a page that does not exist yet. If you want to go the other way and post from a sheet without writing code, see how to auto-post from Google Sheets to social media.
Open the script editor from the sheet, through the extensions menu, and write one function. A web app calls the function named doGet whenever somebody requests its address.
const SHEET_ID = "paste-the-id-of-your-sheet-here";
const SITE = "https://example.com";
function doGet() {
const rows = SpreadsheetApp.openById(SHEET_ID)
.getSheetByName("Posts")
.getDataRange()
.getValues()
.slice(1);
const now = new Date();
const posts = rows
.filter((r) => r[4] === "Published" && r[3] instanceof Date && r[3] <= now)
.sort((a, b) => b[3] - a[3])
.slice(0, 20);
const xml = render(posts);
return ContentService.createTextOutput(xml)
.setMimeType(ContentService.MimeType.RSS);
}The function skips the heading row, keeps only rows with the status Published and a real date in the past, sorts them with the newest first and keeps twenty. Twenty items are enough for a reader or an auto-poster, and a short file is fetched quickly. The mime type tells the receiving program that the output is a feed and not a web page.
Text from a sheet can contain ampersands, angle brackets and quotation marks, and any of them can break an XML document. Escape every value before it goes into the template.
const ENT = {
"&": "&" + "amp;",
"<": "&" + "lt;",
">": "&" + "gt;",
'"': "&" + "quot;",
"'": "&" + "apos;",
};
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ENT[c]);
function render(posts) {
const newest = posts.length ? posts[0][3] : new Date(0);
const items = posts.map((r) => {
const url = SITE + "/blog/" + r[0] + "/";
return "<item>" +
"<title>" + esc(r[1]) + "</title>" +
"<link>" + esc(url) + "</link>" +
"<guid isPermaLink=\"true\">" + esc(url) + "</guid>" +
"<pubDate>" + r[3].toUTCString() + "</pubDate>" +
"<description>" + esc(r[2]) + "</description>" +
"</item>";
}).join("");
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<rss version=\"2.0\"><channel>" +
"<title>Example Journal</title>" +
"<link>" + SITE + "</link>" +
"<description>News and guides from Example Journal.</description>" +
"<lastBuildDate>" + newest.toUTCString() + "</lastBuildDate>" +
items + "</channel></rss>";
}Several details are deliberate. The date method of the language writes the format that the RSS specification expects, and the modern script runtime supports it. The channel date comes from the newest post and not from the clock, so that the file does not change when nothing was published. The identifier of each item is its permanent address, built from the slug in column A and a fixed site constant, so every link is absolute. Check the first item against the real page on your website, because the address that the feed writes must be the address that opens.
Use the deploy menu, choose a new deployment of the type web app, run it as yourself and allow access to anyone. The word anyone is important: if only signed-in users may open it, the auto-poster receives a login page instead of a feed. After you confirm, Google shows an address that ends with exec. That address is your feed. The first time, you will be asked to authorise the script to read your sheet, which is normal.
The address of the test deployment, which ends with dev, runs the latest saved code but only for you. Never give that one to an auto-poster, because it requires your login.
Apps Script is convenient, but it is not a web server, and an honest guide must say what it cannot do.
For a small feed these limits are acceptable. If you need conditional requests, a custom domain or heavy traffic, move the same logic to a small server or a serverless function, as shown in the AWS Lambda feed guide und the Cloudflare Workers feed guide.
Reading a sheet takes time, and a popular feed may be requested often. The script cache service can keep the finished XML for a few minutes, so that most requests are answered without touching the sheet.
function doGet() {
const cache = CacheService.getScriptCache();
let xml = cache.get("feed");
if (!xml) {
xml = render(loadPosts());
cache.put("feed", xml, 300);
}
return ContentService.createTextOutput(xml)
.setMimeType(ContentService.MimeType.RSS);
}The function loadPosts stands for the reading and filtering code shown earlier. The cache holds values up to a limited size, so a feed of twenty short items fits easily, and a lifetime of five minutes means that a new row appears quickly. If you edit a row and want the change to appear at once, you can remove the cached value from the editor.
Because the sheet is edited by hand, the dates need extra care. A date typed as text is not a date, and the script ignores it, which is the safe behaviour. A row with a date in the future stays out until that moment, but the feed is built on request, so the post appears within minutes after the time has passed. If you import old rows, give them their original dates, because a feed full of items with today’s date makes an auto-poster share your whole archive in one afternoon. For the effect of old dates see the guide to feed order and backdated posts.
Request the address with curl, following the redirect, and look at the result instead of assuming that it is right.
curl -sL "https://script.google.com/macros/s/PASTE-YOUR-ID/exec" | head -20
curl -sIL "https://script.google.com/macros/s/PASTE-YOUR-ID/exec"A longer routine is in the feed validation guide, and a shorter one in debugging a feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new entry, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
A spreadsheet can be a perfectly good source for a feed. Keep the columns fixed, use a real date and a status column as a safety switch, escape the text, build absolute links from the slug, return the RSS mime type and deploy the script as a web app that anyone can open. Remember the limits: no custom headers, a redirect and quotas. Test the live address with curl and a validator, then connect it to an auto-poster, and every row you publish reaches your readers and your social channels.
Yes. A doGet function can return text output with the RSS mime type, and a web app deployment gives the function a public address.
Yes, if the deployment allows access for anyone. Test the address in a private window to be sure.
The web app address serves the version that was deployed. Create a new version in the deployment settings after every change.
Not in a script web app. You can cache the result inside the script, but the response headers cannot be customised, so conditional requests are not available.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new rows are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Entwickelt von Internet Solutions, dem Team hinter PostRSS. Jedes Produkt spart Ihnen auf seine Weise Zeit.