
Short answer: To serve an RSS feed from a Go Fiber application, add a GET route for /feed.xml, read the newest published rows from your database, fill a small set of structs that describe the channel and its items, marshal them with the encoding/xml package so every value is escaped, format dates in RFC 1123 with the GMT suffix and send the result with the application/rss+xml content type. Add the Fiber ETag and cache middleware so repeat requests get a 304 answer, test the route with curl and a validator, then give the public address to an auto-poster so every new item reaches your social channels.
Fiber is one of the most popular web frameworks for Go. It is built on fasthttp, it borrows its API style from Express and it is a common choice for APIs, dashboards and small content sites that need to be fast and simple to deploy as a single binary. Sooner or later such a site publishes something that people want to follow: articles, release notes, product listings or event dates. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete Fiber RSS feed with escaping, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems depend on it.
A feed is worth adding whenever the application publishes things that people outside it want to follow:
If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you prefer the standard library router and a feed library instead of a framework, see the Go guide with gorilla/feeds. The approach below uses only Fiber and the standard library, which keeps the dependency list short.
Create a module, add Fiber and a database driver. The example uses SQLite through the modernc driver, which needs no C compiler, but the query and the structs stay the same with PostgreSQL or MySQL.
mkdir feedapp && cd feedapp
go mod init example.com/feedapp
go get github.com/gofiber/fiber/v2
go get modernc.org/sqliteKeep the public address of the site in configuration rather than reading it from the incoming request. Behind a reverse proxy, the request may show an internal host name or a plain http scheme, and a feed full of internal addresses is useless to every reader. The example reads the address from an environment variable with a safe default.
The encoding/xml package turns Go structs into XML and escapes every string on the way. That is the main reason to prefer it over joining strings by hand: a title containing an ampersand or an angle bracket is converted to the proper entity automatically, and the feed stays valid. The structs below describe a minimal RSS 2.0 document with the self link from the Atom namespace, which validators expect.
package main
import "encoding/xml"
type RSS struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Atom string `xml:"xmlns:atom,attr"`
Channel Channel `xml:"channel"`
}
type Channel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Language string `xml:"language"`
Self AtomLink `xml:"atom:link"`
Items []Item `xml:"item"`
}
type AtomLink struct {
Href string `xml:"href,attr"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
}
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
GUID GUID `xml:"guid"`
PubDate string `xml:"pubDate"`
Description string `xml:"description"`
}
type GUID struct {
IsPermaLink string `xml:"isPermaLink,attr"`
Value string `xml:",chardata"`
}The GUID struct carries the isPermaLink attribute set to false, which tells readers that the value is an identifier and not an address. That lets you use the database id with a fixed prefix, which never changes even when an editor changes a slug.
The handler queries the published rows, builds the structs, marshals them and writes the response. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored in UTC.
package main
import (
"database/sql"
"encoding/xml"
"log"
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/etag"
_ "modernc.org/sqlite"
)
func main() {
siteURL := os.Getenv("SITE_URL")
if siteURL == "" {
siteURL = "https://www.example.com"
}
db, err := sql.Open("sqlite", "data/site.db")
if err != nil {
log.Fatal(err)
}
app := fiber.New()
app.Use("/feed.xml", etag.New())
app.Use("/feed.xml", cache.New(cache.Config{Expiration: 5 * time.Minute}))
app.Get("/feed.xml", func(c *fiber.Ctx) error {
rows, err := db.Query(`SELECT id, title, slug, summary, published_at
FROM posts WHERE status = 'published' AND published_at <= ?
ORDER BY published_at DESC LIMIT 20`, time.Now().UTC().Unix())
if err != nil {
return fiber.ErrInternalServerError
}
defer rows.Close()
feed := RSS{Version: "2.0", Atom: "http://www.w3.org/2005/Atom", Channel: Channel{
Title: "Example News",
Link: siteURL + "/",
Description: "The latest posts from Example News",
Language: "en",
Self: AtomLink{Href: siteURL + "/feed.xml", Rel: "self", Type: "application/rss+xml"},
}}
for rows.Next() {
var id int64
var title, slug, summary string
var published int64
if err := rows.Scan(&id, &title, &slug, &summary, &published); err != nil {
return fiber.ErrInternalServerError
}
feed.Channel.Items = append(feed.Channel.Items, Item{
Title: title,
Link: siteURL + "/" + slug + "/",
GUID: GUID{IsPermaLink: "false", Value: "post-" + itoa(id)},
PubDate: time.Unix(published, 0).UTC().Format(time.RFC1123),
Description: summary,
})
}
out, err := xml.MarshalIndent(feed, "", " ")
if err != nil {
return fiber.ErrInternalServerError
}
c.Set("Content-Type", "application/rss+xml; charset=utf-8")
return c.SendString(xml.Header + string(out))
})
log.Fatal(app.Listen(":3000"))
}The itoa helper is a one-line wrapper around strconv.FormatInt, left out for brevity. Run the program, open the route in a browser or with curl, and you should see well-formed XML with at most twenty items, newest first. The xml.Header constant adds the XML declaration with UTF-8, which is the encoding Go writes.
| Feed field | Source in the example | Rule to keep |
|---|---|---|
| title | The title column | Plain text, escaped, no markup |
| link | Site address plus slug | Absolute address that returns 200 directly |
| guid | The row id with a prefix | Never changes, even if the slug changes |
| pubDate | The publication timestamp in UTC | RFC 1123 with GMT, never in the future |
| description | The summary column | One or two sentences that make sense alone |
The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. Using the database id with a fixed prefix avoids that. The problem and its fixes are described in why a feed auto-posts the same article twice.
RSS dates follow RFC 822, with English abbreviations for days and months. The time.RFC1123 layout in Go prints exactly that shape, and because the time value is converted to UTC first, the zone is written as GMT, which every parser accepts. Go never localises month names, so there is no risk of a server locale producing names that readers cannot parse. Do not use RFC 3339 here; that format belongs to Atom feeds, and many RSS readers reject it. If a validator complains about dates, read the guide to RSS date format errors.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The two middlewares registered on the feed route handle that. The etag middleware computes a hash of the response body, sets the ETag header and answers a request whose If-None-Match header matches with a 304 and no body. The cache middleware keeps the rendered body in memory for five minutes, so the database is queried once per period rather than once per request. Together they cut the cost of each check to almost nothing.
Choose the cache period with care. A long cache delays every new post by the same amount, because the feed keeps serving the old list until it expires. A few minutes is a good balance for most sites. If you run several instances behind a load balancer, the in-memory cache is per instance, which is fine for a feed. The background to both techniques is in the guide to ETags and conditional GET dan the guide to feed caching.
In production, build a single binary, run it under a process manager and put a reverse proxy in front of it that terminates HTTPS. Keep the public address in your own setting rather than trusting forwarded headers, so that links in the feed are always correct. Check that the proxy does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.
An auto-poster is more useful when each item carries a picture. Add an Enclosure struct with the address, the byte length and the type of the image, tagged as an enclosure element with three attributes, and fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags. If a summary contains HTML, keep it escaped as the marshaller does, or wrap it in a CDATA block as described in the CDATA guide; both are valid, and readers handle both.
There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl. The Fiber documentation describes the middleware options in detail.
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 title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, 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, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
Fiber needs only a route, a query and a handful of structs to serve a dependable RSS feed: select published items dated in the past, let encoding/xml escape every value, use a stable GUID, format dates in RFC 1123 UTC, build links from a configured public address and send the result with the right content type behind the ETag and cache middlewares. Test the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.
No. Fiber has no feed support, but the encoding/xml package from the Go standard library produces a valid feed from a few structs, and Fiber adds the ETag and cache middlewares that make the route cheap to poll.
The marshaller escapes every string, so a title with an ampersand or an angle bracket cannot break the XML. A template works too, but you must remember to escape every value yourself.
It hashes the response, sets the ETag header and compares it with the If-None-Match header of the next request. When they match, the route answers 304 with no body, which saves bandwidth for both sides.
Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.
Give every item a stable identifier and use it as the GUID. Avoid editing the publication date of items that are already public, because some tools treat a changed date as a new item.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Dibina oleh Internet Solutions, pasukan di sebalik PostRSS. Setiap produk menjimatkan masa anda dengan cara tersendiri.