Generating an Atom feed using Go

See Atom feeds for a summary of the Atom specification.

See notes with the “rss” tag for other implementations.

This implementation uses the atom package from the golang.org/x/tools module.

Setup

Create an initial main.go:

package main

import (
    "encoding/xml"
    "fmt"

    "golang.org/x/tools/blog/atom"
)

func main() {
    feed := atom.Feed{}
    out, _ := xml.Marshal(feed)
    fmt.Println(string(out))
}

Fetch the dependency and vendor it. Then check that everything is set up:

go mod init test
go mod tidy
go mod vendor
go run .

Creating a feed

In main.go, set the desired metadata on the Feed:

updated := parseDate("2025-11-02")
feed := atom.Feed{
    Title: "My Blog",
    ID:    "tag:example.com,2025-11:blog",
    Link: []atom.Link{
        {Rel: "self", Href: "https://www.example.com/feed.atom"},
        {Rel: "alternate", Href: "https://www.example.com/"},
    },
    Updated: atom.Time(updated),
    Author: &atom.Person{
        Name: "AN Author",
        URI:  "https://www.example.com/about.html",
    },
}

This makes use of a helper function:

func parseDate(date string) time.Time {
    t, err := time.Parse("2006-01-02", date)
    if err != nil {
        log.Fatal(err)
    }
    return t.Add(12 * time.Hour)
}

Note that the following metadata cannot be set using this library, unlike with FeedGenerator:

Neither library supports adding the following:

Adding entries

For each post, after parsing the metadata and reading in the body, create an Entry and append it to the feed:

body := `<h1>New year, new blog</h1>
<p>Let me tell you all about my blogging setup!</p>`
entry := &atom.Entry{
    Title: "New year, new blog",
    ID:    makeTagUri("www.example.com", "/first-post/", "2025-01-01"),
    Link: []atom.Link{
        {Rel: "alternate", Href: "https://www.example.com/first-post/"},
    },
    Published: atom.Time(parseDate("2025-01-01")),
    Updated:   atom.Time(parseDate("2025-11-02")),
    Summary:   &atom.Text{Body: "My blogging setup", Type: "html"},
    Content:   &atom.Text{Body: body, Type: "html"},
}
feed.Entry = append(feed.Entry, entry)

This makes use of another helper function:

func makeTagUri(hostname, path, date string) string {
    return fmt.Sprintf("tag:%s,%s:%s", hostname, date, path)
}

Note that the following metadata cannot be set using this library, unlike with FeedGenerator:

Writing to disk

In order to add the XML header to the output, as well as some indentation for readability, let’s use another helper function:

func main() {
  // ...
    writeXML(os.Stdout, &feed)
}

func writeXML(w io.Writer, feed *atom.Feed) {
    _, err := w.Write([]byte(xml.Header))
    if err != nil {
        log.Fatal("write xml header: ", err)
    }
    enc := xml.NewEncoder(w)
    enc.Indent("", "  ")
    err = enc.Encode(feed)
    if err != nil {
        log.Fatal("xml encode: ", err)
    }
}

The resulting output looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>My Blog</title>
  <id>tag:example.com,2025-11:blog</id>
  <link rel="self" href="https://www.example.com/feed.atom"></link>
  <link rel="alternate" href="https://www.example.com/"></link>
  <updated>2025-11-02T12:00:00+00:00</updated>
  <author>
    <name>AN Author</name>
    <uri>https://www.example.com/about.html</uri>
  </author>
  <entry>
    <title>New year, new blog</title>
    <id>tag:www.example.com,2025-01-01:/first-post/</id>
    <link rel="alternate" href="https://www.example.com/first-post/"></link>
    <published>2025-01-01T12:00:00+00:00</published>
    <updated>2025-11-02T12:00:00+00:00</updated>
    <summary type="html">My blogging setup</summary>
    <content type="html">&lt;h1&gt;New year, new blog&lt;/h1&gt;&#xA;&lt;p&gt;Let me tell you all about my blogging setup!&lt;/p&gt;</content>
  </entry>
</feed>