sitemap

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 20 Imported by: 0

README

go-sitemap-parser

codecov Go Go Reference OpenSSF Scorecard Mentioned in Awesome Go

A Go package to parse XML Sitemaps compliant with the Sitemaps.org protocol.

For information on reporting security vulnerabilities, see SECURITY.md.

Features

  • Recursive parsing (sitemap index → sitemaps → URLs)
  • The sitemaps a call finds can be queried, whether or not they were fetched, with the date a sitemap index gives for each
  • The date a page was last modified is returned for the pages of RSS and Atom feeds as well (<pubDate>, <updated>)
  • Iterators over the URLs and the errors of a call (URLs(), Errors()), the URLs as copies that are the caller's own
  • Concurrent (multi-threaded) fetching and parsing
  • Configurable follow rules to filter which sitemaps to parse
  • Configurable URL rules to filter which URLs to include
  • Configurable HTTP response size limit
  • Limits on the sitemaps a call fetches and on the URLs it collects, so that a call comes to an end whatever a server lists
  • A limit on the validation errors a call records, so that its errors are bounded like its results are
  • Tolerant mode (default): resolves relative URLs in <loc> elements; rejects URLs exceeding 2,048 characters after resolution
  • Strict mode: validates URLs per the sitemaps.org specification
  • A value that cannot be parsed (e.g. a malformed <lastmod>) costs only itself, never the whole sitemap
  • Google Image Sitemap extension (<image:image>)
  • Google News Sitemap extension (<news:news>)
  • Google Video Sitemap extension (<video:video>)
  • XHTML hreflang extension (<xhtml:link>)
  • XML documents in encodings other than UTF-8 (e.g. ISO-8859-1, windows-1252) are transcoded automatically
  • Typed errors: *ConfigError, *NetworkError, *ParseError, *ValidationError — inspectable via errors.As
  • A limit that was reached can be told with errors.Is: ErrMaxResponseSize, ErrMaxDepth, ErrMaxSitemaps, ErrMaxURLs, ErrMaxValidationErrors
  • Thread-safe

Formats supported

  • robots.txt
  • XML .xml
  • RSS 2.0
  • Atom 1.0
  • Plain text .txt
  • Gzip compressed files (e.g., .xml.gz, .txt.gz)

XML documents do not have to be UTF-8 encoded, and a document may begin with a UTF-8 byte order mark, see Character encoding.

A document of any of these formats may be gzip compressed. A gzip file is read to the end of its last member: a file that was appended to, or put together from several (cat a.gz b.gz), holds a member for each part, and its content is that of all of them. What follows the last member and is no member, such as a newline, is ignored. Content that cannot be decompressed to its end is not parsed, and is reported once, as a *ParseError (gzip decompression failed: unexpected EOF). See examples/gzip for a runnable example.

A robots.txt is recognised by the URL passed to Parse(): the document is read as one when the last segment of the path of that URL is robots.txt. The letter case makes no difference, and neither does a query or a fragment that follows the path, so https://example.com/robots.txt?v=2 and https://example.com/ROBOTS.TXT are read as a robots.txt too. What follows the path is no part of it: https://example.com/sitemap.xml?from=/robots.txt is the URL of a sitemap, and is parsed as one. It is the URL passed in that decides, not the one a redirect leads to, and only the document passed to Parse() can be a robots.txt: one that a sitemap index lists is parsed as a sitemap.

Of a robots.txt only the Sitemap: lines are read. The name of the directive is not case-sensitive, the line may be indented, and what follows a # is a comment. The sitemaps the lines name are fetched and parsed; how their URLs are checked is described under Strict mode. See examples/robots for a runnable example.

Of a feed, the location of a page and the date it was last modified are taken, of a text sitemap the location only. ChangeFreq and Priority are nil for their URLs, and they have nothing of the extensions:

  • RSS: the <link> of every <item>, and the <pubDate> of the item as LastMod, see examples/rss
  • Atom: for every <entry>, the first <link> with rel="alternate" or without a rel attribute, and the <updated> of the entry as LastMod, see examples/atom
  • Plain text: every line that begins with http:// or https://; empty lines, lines that begin with # and every other line are skipped, see examples/text. LastMod is nil

The date of a feed entry is read in the format its feed prescribes:

  • <pubDate> holds a date and time of RFC 822, such as Mon, 15 Jan 2024 10:30:00 GMT. The day of the week and the seconds may be left out, the year has four digits or two (24 is 2024, 69 to 99 are 1969 to 1999), and the time zone is an offset (+0100) or one of the names UT, GMT, UTC, Z, EST, EDT, CST, CDT, MST, MDT, PST and PDT. A name stands for the zone the RFC says it does, whatever the time zone of the machine is. Another name (CET) is not known, and the date is not read
  • <updated> holds a date in one of the formats of a <lastmod>, among them the one Atom prescribes (RFC 3339, 2024-01-15T10:30:00Z)

An entry without a date has none, and so has one whose date is empty. A date in another format is left unset (nil) and reported as a *ValidationError, and the page is returned all the same, in both modes, see Strict mode. Only the date of the entry itself is read: neither the dates of the feed (<lastBuildDate> and the <pubDate> of an RSS channel, the <updated> of an Atom feed) nor the <published> of an Atom entry are.

Requirements

Go 1.26 or later.

Installation

go get github.com/aafeher/go-sitemap-parser
import "github.com/aafeher/go-sitemap-parser"

Usage

Create instance

To create a new instance with default settings, you can simply call the New() function.

s := sitemap.New()
Configuration defaults
  • userAgent: "go-sitemap-parser (+https://github.com/aafeher/go-sitemap-parser/blob/main/README.md)"
  • fetchTimeout: 3 seconds
  • maxResponseSize: 52428800 (50 MB)
  • maxDepth: 10
  • maxConcurrency: 16
  • maxSitemaps: 50000
  • maxURLs: 10000000
  • maxValidationErrors: 100000
  • multiThread: true
  • strict: false
  • httpClient: nil (a default *http.Client is created per call with the configured fetchTimeout)
Overwrite defaults
User Agent

To set the user agent, use the SetUserAgent() function.

s := sitemap.New()
s = s.SetUserAgent("YourUserAgent")

... or ...

s := sitemap.New().SetUserAgent("YourUserAgent")
Fetch timeout

To set the fetch timeout, use the SetFetchTimeout() function. It should be specified in seconds as a uint16 value (1–65535 seconds). A value of 0 is rejected and a *ConfigError is recorded. Note: when a custom HTTP client is set via SetHTTPClient(), this value has no effect — the client's own Timeout field controls the request deadline.

s := sitemap.New()
s = s.SetFetchTimeout(10)

... or ...

s := sitemap.New().SetFetchTimeout(10)
Max response size

To set the maximum allowed HTTP response size, use the SetMaxResponseSize() function. It should be specified in bytes as an int64 value. The default is 50 MB, matching the sitemaps.org protocol limit. Responses exceeding this limit will result in an error.

The same limit caps the decompressed size of gzip-compressed content, of all the members of a gzip file together, so a small .gz response cannot expand without bound in memory. This applies both to fetched content and to gzip content passed in through the urlContent argument of Parse(). Content that expands beyond the limit is rejected and reported via GetErrors() as a *ParseError. If it is the content of the URL passed to Parse(), the call fails with that error, see Parse.

Both errors wrap ErrMaxResponseSize, the *NetworkError of a response over the limit and the *ParseError of gzip content over it, see Limit errors.

The value must be greater than 0. Unlike with SetMaxSitemaps(), SetMaxURLs() and SetMaxValidationErrors(), 0 does not lift the limit: it is rejected with a *ConfigError and the limit stays as it was. Every value above it is a limit, up to math.MaxInt64, which nothing can exceed.

s := sitemap.New()
s = s.SetMaxResponseSize(10 * 1024 * 1024) // 10 MB

... or ...

s := sitemap.New().SetMaxResponseSize(10 * 1024 * 1024) // 10 MB
Max depth

To set the maximum recursion depth for following sitemap indexes, use the SetMaxDepth() function. A sitemap index may reference other sitemap indexes; this limits how many levels deep the parser will follow. The default is 10.

s := sitemap.New()
s = s.SetMaxDepth(5)

... or ...

s := sitemap.New().SetMaxDepth(5)

With SetMaxDepth(1) the sitemaps listed by the document passed to Parse() are fetched, but if one of them is a sitemap index itself, the sitemaps it lists are not. A robots.txt does not count as a level: the sitemaps it names are treated like the document passed to Parse().

When the limit is reached, a *ParseError is recorded in GetErrors() for every sitemap index whose sitemaps are not followed. It names that sitemap index, by the URL it was served from:

parse "https://example.com/sitemap-index-2.xml": max recursion depth of 1 reached

The error wraps ErrMaxDepth, see Limit errors. Reaching the limit does not fail Parse(): the URLs collected up to that depth are returned.

See examples/maxdepth for a runnable example.

Max sitemaps

A sitemap index may list sitemap indexes, which may list further ones, so how many sitemaps a call ends up fetching is up to the server. To limit the number of sitemaps a Parse() / ParseContext() call fetches, use the SetMaxSitemaps() function.

The value is an int:

  • 0: no limit.
  • a positive value: the call fetches at most that many sitemaps. The default is 50000, which is as many sitemaps as a sitemap index may list according to the sitemaps.org protocol.
s := sitemap.New()
s = s.SetMaxSitemaps(1000)

... or ...

s := sitemap.New().SetMaxSitemaps(1000)

The limit is one of the call, not of a sitemap index: the sitemaps a robots.txt or a sitemap index lists count on all levels together.

  • Every sitemap the call requests counts, whether or not the request succeeds.
  • A sitemap that is listed more than once is requested, and counted, once.
  • The document passed to Parse() does not count.

Once the limit is reached, the sitemaps that are left are not fetched, and a *ParseError is recorded in GetErrors(), once for the call. It names the URL passed to Parse():

parse "https://example.com/sitemap-index.xml": limit of 1000 sitemaps reached

The error wraps ErrMaxSitemaps, see Limit errors. Reaching the limit does not fail Parse(): the URLs of the sitemaps fetched until then are returned. With multi-threading off, the sitemaps are fetched in the order they are listed, so it is the first ones that are fetched. With multi-threading on, which ones are fetched depends on how fast the server answers. Nothing is recorded if no sitemap had to be left out.

The same limit is on the sitemaps GetSitemaps() returns, which are the ones the documents list, whether or not they are fetched. No more of them are kept than the limit allows, and the same error is recorded if one had to be left out. The two are counted apart, so a call that fetches fewer sitemaps than it finds, as one with follow rules does, can reach the limit by the sitemaps it finds: with a limit of 1,000, a call for a sitemap index of 1,200 entries records the error whichever of the sitemaps are followed.

Negative values are rejected and an error is recorded in GetErrors(). The limit of a call is the one set when the call starts.

See examples/maxsitemaps for a runnable example.

Max URLs

The URLs of a call are kept in memory until the next call, and how many there are is up to the server as well. To limit the number of URLs a Parse() / ParseContext() call collects, use the SetMaxURLs() function.

The value is an int:

  • 0: no limit.
  • a positive value: the call collects at most that many URLs. The default is 10000000.
s := sitemap.New()
s = s.SetMaxURLs(100000)

... or ...

s := sitemap.New().SetMaxURLs(100000)

The limit is one of the call as well: the URLs of all the sitemaps count together. What counts are the URLs that GetURLs() returns: an entry that is not valid does not count, and neither does a URL that the patterns set with SetRules() leave out.

Once the limit is reached, the call stops collecting:

  • what is left of the document that reached the limit is left out unread, so it adds no errors either;
  • the sitemaps that are left are not fetched, and a sitemap that was under way is not parsed;
  • a *ParseError is recorded in GetErrors(), once for the call. It names the URL passed to Parse():
parse "https://example.com/sitemap-index.xml": limit of 100000 URLs reached

The error wraps ErrMaxURLs, see Limit errors. Reaching the limit does not fail Parse(): the URLs collected until then are returned. With multi-threading off, they are the first ones in the order the sitemaps list them. With multi-threading on, which ones they are depends on how fast the server answers. Nothing is recorded if nothing had to be left out.

The default is a last resort rather than a tight bound: ten million URLs take up gigabytes of memory. Set a lower limit when the documents are not trusted, see SECURITY.md.

Negative values are rejected and an error is recorded in GetErrors(). The limit of a call is the one set when the call starts.

See examples/maxurls for a runnable example.

Max validation errors

What is wrong with an entry is recorded in GetErrors() as a *ValidationError: a location that is not valid, a value that cannot be parsed, in strict mode a value the protocol does not allow. An entry that is not valid does not count towards SetMaxURLs(), so how many of these errors a call records is up to the server as well: a single document of 50 MB can hold some three million entries without a location. To limit the number of validation errors a Parse() / ParseContext() call records, use the SetMaxValidationErrors() function.

The value is an int:

  • 0: no limit.
  • a positive value: the call records at most that many validation errors. The default is 100000.
s := sitemap.New()
s = s.SetMaxValidationErrors(1000)

... or ...

s := sitemap.New().SetMaxValidationErrors(1000)

The limit is one of the call as well: the validation errors of all the documents count together. What counts are the *ValidationError values GetErrors() returns, so an entry with two values that are not valid counts twice.

Once the limit is reached, the validation errors that follow are not recorded, and a *ParseError is recorded in GetErrors(), once for the call. It names the URL passed to Parse():

parse "https://example.com/sitemap-index.xml": limit of 1000 validation errors reached

The error wraps ErrMaxValidationErrors, see Limit errors. Nothing is recorded if no error had to be left out.

Unlike the limits on the sitemaps and on the URLs, this one takes nothing off what the call does:

  • the documents are read to their end and the sitemaps are fetched all the same, so GetURLs() and GetSitemaps() return what they return without the limit;
  • the errors of the other types are recorded whatever the limit is: a sitemap that cannot be fetched or parsed is reported even when there is no room left for validation errors. How many of those there are depends on the sitemaps the call fetches, not on the entries of a document, so it is SetMaxSitemaps() that bounds them.

With multi-threading off, the validation errors recorded are the first ones in the order the documents are read. With multi-threading on, which ones they are depends on how fast the server answers.

Negative values are rejected and an error is recorded in GetErrors(). The limit of a call is the one set when the call starts.

See examples/maxvalidationerrors for a runnable example.

Max concurrency

When multi-threaded parsing is enabled, the sitemaps a sitemap index or a robots.txt lists are fetched and parsed concurrently. For very large sitemap indexes this can mean a large number of goroutines, HTTP connections and documents in memory at once. To bound the number of sitemaps that are worked on at the same time across the whole Parse() / ParseContext() call, use the SetMaxConcurrency() function.

The value is an int:

  • 0: unlimited concurrency.
  • a positive value: at most that many sitemaps are being fetched or parsed at any time. The default is 16.

A sitemap counts from the moment its request is sent until its content is parsed, so the limit bounds the connections that are open, the CPU cores that are busy and the documents that are held in memory at once. Parsing a sitemap takes several times the memory of the document itself; a lower limit trades speed for a smaller footprint.

A sitemap that waits for its turn takes up none of these, and no goroutine either: a goroutine is started for a sitemap only when there is a slot for it. However many sitemaps an index lists, the goroutines of the parser are those of the sitemaps being worked on, plus one for every sitemap index whose sitemaps are not finished yet. With SetMaxConcurrency(0) a goroutine is started, and a request sent, for every sitemap listed at once.

Negative values are rejected and an error is recorded in GetErrors().

s := sitemap.New()
s = s.SetMaxConcurrency(8)

... or ...

s := sitemap.New().SetMaxConcurrency(8)

Cancelling the supplied context.Context while sitemaps wait for a slot ends the wait at once. The sitemaps that were still waiting are not fetched, and nothing is recorded for them: that the call was cut short is reported once for the call, see Parse with context.

Multi-threading

By default, the package uses multi-threading to fetch and parse sitemaps concurrently. To set the multi-thread flag on/off, use the SetMultiThread() function.

With multi-threading on, each sitemap is fetched, unzipped and decoded by a goroutine of its own, so the sitemaps of an index are parsed on as many CPU cores as SetMaxConcurrency() allows. The URLs of a sitemap stay together in GetURLs(), in the order the sitemap lists them; which sitemap comes first depends on which one is finished first.

With multi-threading off, the sitemaps are fetched and parsed one at a time and in the order they are listed, so no more than one request is in flight at any time. This goes for the sitemaps a robots.txt names as well as for those of a sitemap index. SetMaxConcurrency() has no effect in this case.

s := sitemap.New()
s = s.SetMultiThread(false)

... or ...

s := sitemap.New().SetMultiThread(false)

See examples/multithread for a runnable example.

Follow rules

To set the follow rules, use the SetFollow() function. It should be specified a []string value. It is a list of regular expressions. Only the sitemaps whose URL matches one of these expressions are fetched and parsed, whether a sitemap index lists them or a robots.txt names them. A sitemap index that is not followed takes the sitemaps it lists with it. The URL passed to Parse() is always fetched. A sitemap that is not followed is still among the sitemaps GetSitemaps() returns. If no follow rules are provided, all sitemaps are followed. Patterns longer than 1,000 characters are rejected and reported via GetErrors().

The expressions are matched against the absolute URL of a sitemap, a relative one being resolved first. In tolerant mode that is the URL as it is requested: with a space or a character that is not in ASCII percent-encoded (m%C3%BCnchen, not münchen), and with an internationalised host name in its Punycode form. They match anywhere in the URL unless they are anchored: example\.com also matches https://example.com.evil.test/sitemap.xml, ^https://example\.com/ does not.

s := sitemap.New()
s.SetFollow([]string{
	`\.xml$`,
	`\.xml\.gz$`,
})

... or ...

s := sitemap.New().SetFollow([]string{
	`\.xml$`,
	`\.xml\.gz$`,
})

See examples/follow for a runnable example.

URL rules

To set the URL rules, use the SetRules() function. It should be specified a []string value. It is a list of regular expressions. Only URLs that match one of these expressions will be included in the final result. The expressions are matched against the URL as GetURLs() returns it. In tolerant mode that is the URL with a space or a character that is not in ASCII percent-encoded (m%C3%BCnchen, not münchen), and with an internationalised host name in its Punycode form. If no rules are provided, all URLs found are included. Patterns longer than 1,000 characters are rejected and reported via GetErrors().

s := sitemap.New()
s.SetRules([]string{
	`product/`,
	`category/`,
})

... or ...

s := sitemap.New().SetRules([]string{
	`product/`,
	`category/`,
})
HTTP client

To use a custom HTTP client for all requests, use the SetHTTPClient() function. This is useful when you need a custom transport, proxy, TLS configuration, or authentication via a custom http.RoundTripper.

When a custom client is provided, SetFetchTimeout has no effect — the client's own Timeout field controls the request deadline. Pass nil to reset to the default client behaviour.

s := sitemap.New()
s = s.SetHTTPClient(&http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
    },
})

... or ...

s := sitemap.New().SetHTTPClient(&http.Client{Timeout: 30 * time.Second})

See examples/httpclient for a runnable example.

Strict mode

By default, the parser operates in tolerant mode: relative URLs found in <loc> elements are automatically resolved against the parent sitemap URL. This handles real-world sitemaps that may not fully comply with the specification.

To enable strict mode, use the SetStrict() function. In strict mode, all URL entries are validated per the sitemaps.org protocol:

  • <loc> must be an absolute HTTP or HTTPS URL
  • <loc> must use the same host and protocol as the sitemap file (for a sitemap reached through a redirect: as the URL it was served from, see Redirects); what counts as the same host is described below
  • <loc> must hold neither a space nor a control character (a tab or a line break for one); a URL gives them percent-encoded, a space as %20
  • <loc> must hold none of the characters " < > \ ^ ` { | } either, which have no place in a URL (RFC 3986); a URL gives them percent-encoded as well, a vertical bar as %7C. Square brackets and characters that are not in ASCII are accepted
  • <loc> must not exceed 2,048 characters
  • <changefreq> must be one of the values of the protocol, written as the protocol writes them: always, hourly, daily, weekly, monthly, yearly or never (if present)
  • <priority> must be between 0.0 and 1.0 inclusive (if present)
  • <lastmod> and <priority> of a <url> must hold a value that can be parsed (if present)
  • The document must be well-formed XML, otherwise it is rejected as a whole and reported as a *ParseError

In tolerant mode (the default):

  • Relative <loc> URLs are resolved against the parent sitemap URL
  • A space in a <loc> URL is percent-encoded (%20) wherever it stands, so that the URL can be requested as it is: https://example.com/search?q=summer sale is returned as https://example.com/search?q=summer%20sale
  • A character that is not in ASCII is percent-encoded the same way, as the bytes it is in UTF-8, in the query as well as in the path and in the fragment: https://example.com/café?q=café is returned as https://example.com/caf%C3%A9?q=caf%C3%A9
  • An internationalised host name in a <loc> URL is returned in its Punycode form, the name a request goes to: https://bücher.example/page is returned as https://xn--bcher-kva.example/page, see Internationalised host names
  • <loc> URLs exceeding 2,048 characters after resolution are rejected; what is percent-encoded counts as the characters it is encoded to
  • The URLs of the extensions (of an image, a video or an alternate link) are percent-encoded like a <loc> URL where they are absolute HTTP or HTTPS URLs, and are returned as the document gives them where they are not, see GetURLs
  • <priority> values outside [0.0, 1.0] are accepted as-is
  • A <lastmod> or <priority> that cannot be parsed is left unset (nil) and reported, the entry itself is kept
  • A <changefreq> value of the protocol that is written in another letter case (Daily, WEEKLY) is read as the value itself (daily, weekly), so that it equals its constant; a value the protocol does not know is kept as the document gives it
  • XML mistakes that can be read past are accepted: an unescaped &, an unknown entity such as &nbsp;, a missing end tag

Entries that fail validation are skipped and reported via GetErrors().

Strict mode takes a URL to be on the host of the sitemap when it names the same host and the same port. The name of a host is not case-sensitive, and a URL that names no port is on the default port of its protocol (80 for HTTP, 443 for HTTPS). In a sitemap at https://example.com/sitemap.xml, https://Example.com/page and https://example.com:443/page are therefore accepted, while a URL on another host, on a subdomain (https://www.example.com/page) or on another port (https://example.com:8443/page) is rejected:

validate "https://example.com:8443/page": strict mode: host "example.com:8443" does not match sitemap host "example.com"

The same goes for the sitemaps a sitemap index lists. An internationalised host name is one host in whichever form it is written, see Internationalised host names.

Strict mode rejects a URL with a character in it that a URL may not hold wherever the character stands, in the path, in the query or in the fragment. The error tells what the URL has to give the character as:

validate "https://example.com/search?tags=summer|sale": strict mode: URL contains the character '|', which it has to give as "%7C"

Tolerant mode accepts such a URL. It returns the character percent-encoded in the path and in the fragment, and as it is in the query.

An entry without a location is one of them in both modes. A <url> or <sitemap> whose <loc> is missing, empty or holds only whitespace is skipped and reported as a *ValidationError. The entry has no URL of its own, so the error names the sitemap it stands in:

validate "https://example.com/sitemap.xml": <loc> of an entry is empty or missing

An RSS <item> without a <link> and an Atom <entry> without a link are skipped without an error, since a feed item is not required to have one.

A location has to name a host in both modes as well: a page is on one, and so is a sitemap. https:///page, https:page and https://:8080/page, which names a port but no host, are skipped and reported as a *ValidationError. A sitemap index entry or a Sitemap: line of a robots.txt with such a URL is not requested:

validate "https://:8080/page": missing host

In tolerant mode this goes for a relative URL too. One that begins with // gives the host itself and takes only the protocol from the sitemap: //cdn.example.com/page is https://cdn.example.com/page in a sitemap served over HTTPS. // and ///page therefore name no host and are rejected the same way. A relative URL that does not resolve to a URL at all is rejected with the reason net/url gives:

validate "//::": parse "https://::": invalid port "::" after host

The Loc of every URL that GetURLs() returns is thus an HTTP or HTTPS URL that names a host and that net/url can parse, in both modes.

The sitemaps a robots.txt names are checked before they are fetched as well. The value of a Sitemap: line has to be an HTTP or HTTPS URL of at most 2,048 characters. Tolerant mode resolves a relative one against the URL of the robots.txt and percent-encodes a space or a character that is not in ASCII in it, strict mode requires an absolute one that holds neither a space nor another character a URL may not hold. The sitemap may be on another host than the robots.txt in both modes, which the protocol allows. A value that is rejected is skipped and reported as a *ValidationError:

validate "ftp://example.com/sitemap.xml": unsupported scheme "ftp"

A value that cannot be parsed never costs more than its own entry. The numbers and dates of the extensions (<video:duration>, <video:rating>, <video:view_count>, <video:expiration_date>, <video:publication_date>, <news:publication_date>) are treated the same way in both modes: the field is left unset (nil) and the entry is kept. Every such value is reported via GetErrors() as a *ValidationError for the page it belongs to:

validate "https://example.com/page": invalid <lastmod> value "2024-01-15 10:30:00"

The dates that are not those of a <url> are treated that way in both modes as well: the <lastmod> of an entry of a sitemap index, the <pubDate> of an RSS item and the <updated> of an Atom entry. One that cannot be parsed is left unset and reported, and that is all it costs, in strict mode too: the sitemap is listed and fetched, and the page is returned. The error names the sitemap or the page the date belongs to:

validate "https://example.com/sitemap-1.xml": invalid <lastmod> value "2024-01-15 10:30:00"
validate "https://example.com/blog/post": invalid <pubDate> value "2024-01-15T10:30:00Z"

An empty date is no such value. A <lastmod>, <pubDate>, <updated>, <news:publication_date>, <video:expiration_date> or <video:publication_date> that is empty or holds only whitespace is read as if the element were not there: the field is nil, not the zero time, and nothing is reported in either mode. The exception is the publication date of a news entry, which strict mode requires, see GetURLs. An empty number (<priority>, <video:duration>, <video:rating>, <video:view_count>) reads as 0.

Whitespace around a value is no part of it, in both modes. The content of an element may be indented or stand on a line of its own, and an attribute may be padded: every text value of an entry is returned without the whitespace that surrounds it, those of the extensions included. A value that holds nothing but whitespace is therefore an empty one. A <changefreq> like that is read as if the element were not there, and a value an extension requires is missing then, see GetURLs.

See examples/tolerant for a runnable example of how the two modes treat a sitemap with mistakes in it, an entry without a location, a location without a host and URLs that are not percent-encoded among them, and examples/strict for one of what strict mode requires of a URL and of the images, the videos and the alternate links of an entry.

Internationalised host names

The name of an internationalised host can be written in two forms: in Unicode (bücher.example) and in Punycode (xn--bcher-kva.example), the ASCII form it is looked up by. A sitemap may use either.

Tolerant mode returns a URL with the Punycode form of its host, whichever form the document writes, so that the URLs of one host come back with one name and can be requested as they are:

<loc> returned by tolerant mode
https://bücher.example/page https://xn--bcher-kva.example/page
https://BÜCHER.example/page https://xn--bcher-kva.example/page
https://xn--bcher-kva.example/page https://xn--bcher-kva.example/page
/page in a sitemap at https://bücher.example/sitemap.xml https://xn--bcher-kva.example/page

The name is converted the way net/http converts the host of a URL it requests. This goes for every URL tolerant mode resolves: for the pages of a sitemap of any format, and for the sitemaps a sitemap index lists or a robots.txt names, which are requested by that name. A sitemap that is listed in both forms of its host is therefore fetched once. It goes for the absolute URLs of the extensions too, see GetURLs. A host name in ASCII is returned as it is written, capitals included. So is a name that is no valid domain name, such as bücher_x.example, which comes back the way net/url writes it: percent-encoded, as b%C3%BCcher_x.example.

Strict mode returns a URL the way the document gives it. It takes both forms of the name for the same host: in a sitemap at https://bücher.example/sitemap.xml, https://xn--bcher-kva.example/page is accepted, and so is https://bücher.example/page in a sitemap that was requested by the Punycode form of its host.

See examples/idn for a runnable example.

s := sitemap.New()
s = s.SetStrict(true)

... or ...

s := sitemap.New().SetStrict(true)
Chaining methods

In both cases, the functions return a pointer to the main object of the package, allowing you to chain these setting methods in a fluent interface style:

s := sitemap.New().SetUserAgent("YourUserAgent").SetFetchTimeout(10)
Read configuration

Each configuration setting can be read back via a corresponding Get* method. All getters are thread-safe.

Getter Return type Description
GetUserAgent() string Current user agent string
GetFetchTimeout() uint16 Fetch timeout in seconds
GetMultiThread() bool Whether multi-threaded fetching is enabled
GetMaxResponseSize() int64 Maximum HTTP response size in bytes
GetMaxDepth() int Maximum sitemap index recursion depth
GetMaxConcurrency() int Maximum number of sitemaps fetched or parsed at the same time (0 = unlimited)
GetMaxSitemaps() int Maximum number of sitemaps a call fetches, and of sitemaps it lists (0 = unlimited)
GetMaxURLs() int Maximum number of URLs a call collects (0 = unlimited)
GetMaxValidationErrors() int Maximum number of validation errors a call records (0 = unlimited)
GetFollow() []string Copy of the follow regex pattern list
GetRules() []string Copy of the URL filter regex pattern list
GetHTTPClient() *http.Client Custom HTTP client, or nil if using the default
GetStrict() bool Whether strict validation mode is enabled

GetFollow() and GetRules() return copies — mutating the returned slice does not affect the parser's internal state.

s := sitemap.New().SetMaxConcurrency(8).SetStrict(true)
fmt.Println(s.GetMaxConcurrency()) // 8
fmt.Println(s.GetStrict())         // true
Thread safety

All public methods on *S are safe to call from multiple goroutines. Internal state (configuration, collected URLs, errors) is protected by a mutex.

The mutex is not held while a sitemap is fetched, unzipped or decoded, only while what the sitemap yielded is added to the results. The getters can therefore be called while Parse() is running without waiting for it: GetURLs(), GetURLCount(), GetSitemaps() and GetErrors() return what has been collected so far. The URLs of a sitemap appear all at once, when that sitemap has been parsed, and so do the sitemaps a sitemap index lists.

A loop over URLs() or Errors() goes through what has been collected when it starts. It holds the mutex for no longer than it takes to look at the list, not while it runs, so the body of the loop may call every method of the instance, Parse() among them.

GetURLs() and GetErrors() return the lists the instance keeps, which several goroutines may read and none may change, see GetURLs.

However, two important constraints apply:

  • Concurrent Parse() / ParseContext() calls on the same instance are serialised. A second call blocks until the first completes. If you need to parse multiple sitemaps concurrently, create a separate *S instance per goroutine with New().
  • Configure before parsing. Calling a Set* method while Parse() is running on the same instance is safe (the write is mutex-protected), but the outcome is non-deterministic — the new value may or may not be picked up mid-parse. Set all options before calling Parse().

Deadlock note: a goroutine gives its SetMaxConcurrency slot back as soon as its sitemap is fetched and parsed, before the sitemaps that one lists are followed. This prevents a goroutine from holding a slot while it waits for the slots of the child sitemaps, which would otherwise deadlock when a robots.txt or a sitemap index leads to further sitemap indexes.

Parse

Once you have properly initialized and configured your instance, you can parse sitemaps using the Parse() function.

The Parse() function takes in two parameters:

  • url: the URL of the sitemap to be parsed,
    • url can be a robots.txt or sitemapindex or sitemap (urlset)
    • it is taken for that of a robots.txt when its path ends in robots.txt, with or without a query after it and in any letter case, see Formats supported
  • urlContent: an optional string pointer for the content of the URL.

If you wish to provide the content yourself, pass the content as the second parameter. If not, simply pass nil and the function will fetch the content on its own. The Parse() function performs concurrent parsing and fetching optimized by the use of Go's goroutines and sync package, ensuring efficient sitemap handling.

s, err := s.Parse("https://www.sitemaps.org/sitemap.xml", nil)

In this example, sitemap is parsed from "https://www.sitemaps.org/sitemap.xml". The function fetches the content itself, as we passed nil as the urlContent.

The error Parse() returns is about the document at url itself. It is nil if that document was fetched and parsed, otherwise it tells why it was not:

Error returned Meaning
*ValidationError url is not an HTTP or HTTPS URL with a host (checked when the content is to be fetched)
*NetworkError The document could not be fetched
*ParseError The document could not be parsed: it is not a sitemap, it is empty, its XML or gzip content is broken, it expands beyond the size limit
an untyped error A configuration error is outstanding, so nothing was parsed, see Reusing an instance

Except for the last, the error returned is the very one GetErrors() holds about the document.

What goes wrong further on does not fail the call and is reported via GetErrors() only: a sitemap the document lists that cannot be fetched or parsed, an entry that is not valid, a limit being reached (see Max depth, Max sitemaps and Max URLs). A nil error therefore does not mean that nothing was skipped, and GetErrors() is worth checking after every call:

s, err := sitemap.New().Parse("https://www.sitemaps.org/sitemap.xml", nil)
if err != nil {
    // The document at the URL could not be fetched or parsed.
    log.Fatalf("parse error: %v", err)
}
for _, e := range s.GetErrors() {
    // A sitemap it lists, or an entry, was skipped, or a limit was reached.
    log.Printf("warning: %v", e)
}
Parse with context

For new code, prefer ParseContext() so that callers can propagate cancellation and deadlines to every HTTP request issued by the parser (the initial fetch as well as the recursive sitemap-index/urlset fetches). The legacy Parse() is a backward-compatible wrapper around ParseContext() that uses context.Background().

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

s, err := sitemap.New().ParseContext(ctx, "https://www.sitemaps.org/sitemap.xml", nil)

Cancelling ctx aborts in-flight downloads and prevents new ones from starting. Already-parsed URLs accumulated before cancellation remain available via GetURLs().

A call without a deadline ends when the sitemaps are worked through, or when a limit of the call is reached (see Max sitemaps and Max URLs). The limits bound what a call fetches and collects, not how long it takes: a deadline is what bounds the time.

A call that was cut short returns a *ParseError that names the URL passed to ParseContext() and wraps the error of the context, so errors.Is tells why the call ended:

if errors.Is(err, context.DeadlineExceeded) {
    // the deadline passed
}
if errors.Is(err, context.Canceled) {
    // ctx was cancelled
}

The same error is recorded in GetErrors(), once for the call: in multi-threaded and in sequential mode alike, and however many sitemaps were not fetched. Nothing is recorded for the sitemaps the call did not get to. A request that was under way is reported in addition, as the *NetworkError of its sitemap, which wraps the error of the context as well.

When it is the request for the URL passed to ParseContext() that is cut short, the call returns and records that *NetworkError alone.

See examples/context for a runnable example.

Redirects

HTTP redirects are followed. A sitemap that is reached through a redirect is treated as located at the URL it was finally served from, not at the URL that was requested:

  • relative URLs in it are resolved against that URL (tolerant mode),
  • the URLs it lists have to use the host and protocol of that URL (strict mode),
  • the errors about the document name that URL.

So when http://example.com/sitemap.xml redirects to https://www.example.com/sitemap.xml, a <loc>/page</loc> in it yields https://www.example.com/page. Strict mode accepts the URLs on https://www.example.com and rejects those on http://example.com, the sitemap file not being there.

This goes for the URL passed to Parse() and for every sitemap fetched on the way alike: the sitemaps of a sitemap index and those a robots.txt names. When the content is passed in through urlContent, nothing is fetched and the document is located at the URL given.

Which redirects are followed is up to the HTTP client. The default one stops after 10 consecutive requests, as Go's http.Client does by default; a client set with SetHTTPClient() decides in its CheckRedirect. A fetch that fails is reported as a *NetworkError naming the URL that was requested.

See examples/redirect for a runnable example.

Reusing an instance

An instance can be used for any number of Parse() / ParseContext() calls. Every call starts from a clean state: the URLs, sitemaps and errors collected by the previous call are discarded first, so GetURLs(), GetSitemaps() and GetErrors() always describe the most recent call only. This also holds when a call returns early, for example because the input URL is invalid.

s := sitemap.New()

for _, url := range []string{"https://example.com/sitemap.xml", "https://example.org/sitemap.xml"} {
    if _, err := s.Parse(url, nil); err != nil {
        log.Printf("%s: %v", url, err)
        continue
    }
    fmt.Printf("%s: %d URLs, %d errors\n", url, s.GetURLCount(), s.GetErrorsCount())
}

Configuration errors are the exception, because they belong to the instance rather than to a single call. A *ConfigError recorded by a Set* method stays in GetErrors(), and while any of them is outstanding Parse() does not parse anything and returns the error errors occurred before parsing, see GetErrors() for details. Calling the same setter again with a valid value clears its error:

s := sitemap.New().SetMaxDepth(0) // invalid: recorded as a *ConfigError

_, err := s.Parse(url, nil) // err: errors occurred before parsing, see GetErrors() for details

s.SetMaxDepth(5)           // valid: clears the error recorded for maxDepth
_, err = s.Parse(url, nil) // parses normally

Each setter call replaces the errors recorded by the previous call for the same setting, so repeating an invalid value does not accumulate errors.

See examples/reuse for a runnable example.

Character encoding

The sitemaps.org protocol requires sitemaps to be UTF-8 encoded, but XML documents in other encodings are accepted as well. The parser honours the encoding named in the XML declaration and transcodes the document while reading it, so every string it returns is UTF-8:

// "\xe9" is the letter "é" in ISO-8859-1
content := "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" +
    "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" +
    "  <url><loc>https://example.com/caf\xe9</loc></url>\n" +
    "</urlset>"

s, err := sitemap.New().Parse("https://example.com/sitemap.xml", &content)
// s.GetURLs()[0].Loc == "https://example.com/caf%C3%A9"

This applies to every XML format (sitemap index, urlset, RSS and Atom), in tolerant and in strict mode alike. The encodings of the WHATWG Encoding Standard are supported, among them the ISO-8859 and windows-125x families, US-ASCII, KOI8-R, Shift_JIS, EUC-JP, EUC-KR, GBK, gb18030 and Big5.

A UTF-8 byte order mark (BOM, the bytes EF BB BF) at the beginning of a document is ignored in every format, robots.txt files and plain text sitemaps included, in both modes: it tells the encoding and is no part of the first line. This goes for a gzip compressed document as well. Only the very beginning of a document counts. Anywhere else the character (U+FEFF) is taken as it stands, so a line of a text sitemap or a robots.txt that begins with it is not recognised.

Limitations:

  • A document that declares any other encoding is not parsed; a *ParseError naming the encoding is reported via GetErrors(), and returned by Parse() when it is the document Parse() was called for.
  • Only the XML declaration is consulted. The charset parameter of the HTTP Content-Type header is ignored, and a document without a declared encoding is read as UTF-8.
  • UTF-16 and UTF-32 documents are not supported.
  • Plain text sitemaps and robots.txt files carry no encoding declaration and are not transcoded.

See examples/encoding for a runnable example.

Results

After parsing, you can retrieve the results using the following methods:

GetURLs

Returns all parsed URLs as a []URL slice.

urls := s.GetURLs()

The slice returned is the list the instance keeps, not a copy of it, so a call costs the same however many URLs there are. That makes it a list to read, not one to change:

  • It must not be modified. A URL that is set or moved in it, which is what sorting it does, is set or moved for every other caller as well, and so is a value that is changed through a pointer of a URL (*u.Priority = 1).
  • Appending to it is safe. The slice has no capacity to spare, so append copies it: what is appended reaches neither the instance nor another caller.
  • It does not change afterwards. URLs collected later are in what a later GetURLs() call returns.

For URLs to change and for a list to sort, there is URLs(), which yields each URL as a copy. The slice is never nil.

Each URL struct contains the following fields:

  • Loc (string) — the URL location; never empty, an entry without a <loc> is skipped
  • LastMod (*LastModTime) — last modification time (embeds time.Time), may be nil; also nil when the element is empty or the value cannot be parsed, see Strict mode. It is the <lastmod> of a <url>, the <pubDate> of an RSS item and the <updated> of an Atom entry, see Formats supported; a text sitemap gives no date
  • ChangeFreq (*URLChangeFreq) — change frequency hint, may be nil; also nil when the element is empty. Use the exported constants for comparison: ChangeFreqAlways, ChangeFreqHourly, ChangeFreqDaily, ChangeFreqWeekly, ChangeFreqMonthly, ChangeFreqYearly, ChangeFreqNever. Tolerant mode reads one of them in whatever letter case it is written and keeps a value that is none of them as the document gives it; strict mode skips an entry with either, so a field that is set equals one of the constants there, see Strict mode
  • Priority (*float32) — crawl priority between 0.0 and 1.0, may be nil; also nil when the value cannot be parsed
  • Images ([]Image) — images associated with this URL via the Google Image Sitemap extension, may be nil
  • News (*News) — news metadata associated with this URL via the Google News Sitemap extension, may be nil
  • Videos ([]Video) — videos associated with this URL via the Google Video Sitemap extension, may be nil
  • Hreflangs ([]AlternateLink) — alternate language/region versions of this URL via the XHTML extension, may be nil

No text value of a URL has whitespace around it, see Strict mode. This goes for the fields of the extensions below as well: an Image.Loc or a Video.Title that stands on a line of its own in the document is returned without the line breaks and the indentation.

An image, a video and an alternate link are each known by a URL: Image.Loc, Video.ThumbnailLoc and AlternateLink.Href. It may be on any host. Strict mode requires an absolute HTTP or HTTPS URL that names a host and holds no space, no control character and none of the characters " < > \ ^ ` { | }; an image, a video or an alternate link whose URL is not one is left out and reported, the page itself is kept:

validate "https://cdn.example.com/summer sale.jpg": strict mode: URL contains a space

The other URLs of an image and of a video (Image.License, Video.ContentLoc, Video.PlayerLoc and Video.Uploader.Info) need not be given. Strict mode requires the same of one that is given, and that it is of at most 2,048 characters. One that is not such a URL is reported, and nothing is left out over it: the image or the video is returned, with the URL as the document gives it:

validate "ftp://cdn.example.com/video.mp4": strict mode: unsupported scheme "ftp"

An error names the URL it is about, as these do. An error that is not about a URL names the page the entry is on: that of a video which lacks a field or has one out of range, and that of an image, a video or an alternate link which has no URL at all:

validate "https://example.com/page": strict mode: video <title> is empty
validate "https://example.com/page": strict mode: image <loc> is empty

Tolerant mode asks no more of the URL than that it is there and of at most 2,048 characters. An absolute HTTP or HTTPS URL comes back the way the Loc of the page does, so that it can be requested as it is: with a space or a character that is not in ASCII percent-encoded, and with an internationalised host name in its Punycode form. This goes for every URL of an extension: Image.Loc and Image.License, Video.ThumbnailLoc, Video.ContentLoc, Video.PlayerLoc and Video.Uploader.Info, and AlternateLink.Href.

in the document returned by tolerant mode
https://cdn.example.com/summer sale.jpg https://cdn.example.com/summer%20sale.jpg
https://cdn.example.com/café.jpg?alt=café https://cdn.example.com/caf%C3%A9.jpg?alt=caf%C3%A9
https://bücher.example/cover.jpg https://xn--bcher-kva.example/cover.jpg
https://cdn.example.com/summer%20sale.jpg https://cdn.example.com/summer%20sale.jpg
/images/summer sale.jpg /images/summer sale.jpg

A URL with nothing to encode in it is returned as the document gives it. So is a URL that is no absolute HTTP or HTTPS URL which names a host: a relative one, which is not resolved, one of another protocol, and one that cannot be parsed. So is, finally, a URL that would exceed 2,048 characters percent-encoded. Tolerant mode thus leaves out no image, video or alternate link over the form of its URL. Strict mode returns every URL it accepts as the document gives it.

See examples/tolerant for a runnable example.

Each Image struct contains the following fields (all string):

  • Loc — image URL (required by the spec; images with an empty Loc are silently dropped in tolerant mode, or produce an error in strict mode)
  • Title — image title (optional)
  • Caption — image caption (optional)
  • GeoLocation — geographic location of the image subject (optional)
  • License — URL of the image licence (optional; validated in strict mode if present, see above)

See examples/image for a runnable example.

Each News struct contains:

  • Publication (NewsPublication) — publication metadata:
    • Name (string) — publication name (required in strict mode)
    • Language (string) — BCP 47 language code, e.g. "en" (required in strict mode)
  • PublicationDate (*LastModTime) — article publication date; embeds time.Time, nil if absent or empty (required in strict mode)
  • Title (string) — article title (required in strict mode)

In strict mode, all four required fields (Title, Publication.Name, Publication.Language, PublicationDate) must be present; missing fields are each reported via GetErrors() and the News entry is still included with whatever data was parsed. A <news:publication_date> that is there but empty is reported like a missing one (strict mode: news <publication_date> is empty). In tolerant mode no validation is performed. A PublicationDate that cannot be parsed is left nil and reported in both modes.

See examples/news for a runnable example.

Each AlternateLink struct contains:

  • Rel (string) — relationship, should be "alternate" (strict mode accepts nothing else)
  • Hreflang (string) — language/region code (e.g. "en", "de-ch") (required in strict mode)
  • Href (string) — the URL of the alternate version (required; links with an empty Href are silently dropped in tolerant mode, or produce an error in strict mode)

In strict mode a link whose Rel or Hreflang is not what it has to be is left out and reported, like one whose Href is no URL; the error names the Href of the link.

See examples/hreflang for a runnable example.

Each Video struct contains:

  • ThumbnailLoc (string) — thumbnail image URL (required; videos with an empty ThumbnailLoc are silently dropped in tolerant mode, or produce an error in strict mode)
  • Title (string) — video title (required in strict mode)
  • Description (string) — video description (required in strict mode)
  • ContentLoc (string) — direct URL to the video file (at least one of ContentLoc or PlayerLoc required in strict mode); validated in strict mode if present
  • PlayerLoc (string) — URL of an embedded video player; validated in strict mode if present
  • Duration (*int) — duration in seconds (1–28800); validated in strict mode if present
  • ExpirationDate (*LastModTime) — date after which the video should not be shown; embeds time.Time, nil if absent or empty
  • Rating (*float32) — rating between 0.0 and 5.0; validated in strict mode if present
  • ViewCount (*int) — number of views
  • PublicationDate (*LastModTime) — publication date; embeds time.Time, nil if absent or empty
  • FamilyFriendly (string) — "yes" or "no"
  • Restriction (*VideoRestriction) — country restriction with Relationship ("allow"/"deny") and Value (space-separated country codes)
  • Platform (*VideoPlatform) — platform restriction with Relationship and Value (e.g. "web mobile tv")
  • RequiresSubscription (string) — "yes" or "no"
  • Uploader (*VideoUploader) — uploader name (Value) and optional profile URL (Info); the URL is validated in strict mode if present
  • Live (string) — "yes" or "no"
  • Tags ([]string) — content tags; maximum 32 validated in strict mode

A Duration, Rating, ViewCount, ExpirationDate or PublicationDate that cannot be parsed is left nil and reported via GetErrors() in both modes; the video itself is kept.

In strict mode a video that has a ThumbnailLoc which is a URL is kept whatever else is wrong with it. Every field that is missing, every value that is out of range and every other URL that is no URL is reported via GetErrors(), the first two for the page the video is on, the last for the URL itself.

See examples/video for a runnable example.

URLs

Returns an iterator over the URLs GetURLs() returns, in the same order, to be used in a for loop:

for u := range s.URLs() {
    fmt.Println(u.Loc)
}

Each URL is a copy that is the caller's own. Everything it points to is copied with it (LastMod, ChangeFreq, Priority, Images, News, Videos and Hreflangs, and what a video points to in turn), so nothing that is done to it reaches the URLs the instance keeps. The list is not copied: a loop takes up the memory of one URL at a time, however many URLs there are, and one that ends early costs nothing for the URLs it does not get to.

GetURLs() URLs()
Returns the list the instance keeps an iterator over copies of the URLs
Costs nothing a copy of every URL the loop gets to
The URLs may be changed no yes
len(), access by index yes of the list they are collected into

The iterator is an iter.Seq[URL], so it works with what the standard library has for iterators. slices.Collect() returns the URLs as a list of such copies, which may be sorted and changed:

urls := slices.Collect(s.URLs())
slices.SortFunc(urls, func(a, b sitemap.URL) int {
    return strings.Compare(a.Loc, b.Loc)
})

slices.Collect() cannot tell how long the list is going to be and grows it as it goes, which takes several times the memory of the list where the list is long: 690 MB is allocated on the way to a list of 1,000,000 URLs, which is 120 MB itself. Giving the list its capacity spares that:

urls := slices.AppendSeq(make([]sitemap.URL, 0, s.GetURLCount()), s.URLs())

A loop goes through the URLs as they are when it starts. URLs that are collected while it runs are not among them, and a Parse() call that starts in the meantime does not disturb it. No lock is held while the loop runs, see Thread safety. The iterator can be used for any number of loops, each of which starts anew.

See examples/iterators for a runnable example.

GetURLCount

Returns the number of parsed URLs.

count := s.GetURLCount()
GetRandomURLs

Returns a slice of n randomly selected URLs without duplicates. If n exceeds the number of parsed URLs, all of them are returned, in random order. If n is zero or negative, the result is an empty slice.

randomURLs := s.GetRandomURLs(5)

Each of them is a copy that is the caller's own, with everything it points to, like the URLs URLs() yields: it may be changed.

GetSitemaps

Returns the sitemaps that the documents of the most recent Parse() / ParseContext() call list, as a []Sitemap slice: the Sitemap: lines of the robots.txt the call was made for, and the entries of every sitemap index it read.

for _, sm := range s.GetSitemaps() {
    fmt.Println(sm.Loc)
}

Each Sitemap struct contains the following fields:

  • Loc (string) — the URL the sitemap is requested by, never empty. In tolerant mode it can be requested as it is: a relative URL is resolved against the URL of the document that lists it, a space or a character that is not in ASCII is percent-encoded, and an internationalised host name is in its Punycode form. Strict mode returns it as the document gives it. A sitemap that is reached through a redirect is listed by the URL the document gives, not by the one it was served from
  • LastMod (*LastModTime) — the date the sitemap was last modified according to the sitemap index that lists it, the <lastmod> of its entry (embeds time.Time). It is nil when the entry has none, when the element is empty, and when the value cannot be parsed, which is reported via GetErrors() and costs the sitemap nothing else, see Strict mode. It is nil for a sitemap that only a robots.txt names as well, which gives no date. When the documents of a call give more than one date for a sitemap, it is the latest of them

What is among them, and what is not:

  • A sitemap is listed whether or not it was fetched: one that the follow rules leave out, one that could not be fetched, and one that a limit of the call kept from being fetched (see Max depth, Max sitemaps and Max URLs) are all listed. What a sitemap index lists is known only if the index itself was fetched.
  • A sitemap is listed once, however many documents list it.
  • A location that is rejected (an empty one, one that is no HTTP or HTTPS URL, one that strict mode does not accept) is reported via GetErrors() and not listed.
  • The document passed to Parse() is not listed, unless a document of the call lists it.

The sitemaps of a document are in the order the document lists them. With multi-threading off the documents are read one after the other, so the order is the same from one call to the next; with multi-threading on it is not for the sitemaps of different documents.

No more sitemaps are returned than SetMaxSitemaps() allows (default: 50,000). If the documents list more, the first ones the call found are returned, and the error limit of N sitemaps reached is recorded. The sitemaps listed and the sitemaps fetched are counted apart, so when the limit is reached the ones that are returned need not be the ones that were fetched.

The slice returned is a copy, the dates in it included: it may be modified, and it does not change afterwards. It is never nil.

To find out which sitemaps a sitemap index lists without fetching any of them, set a follow rule that no URL matches:

s := sitemap.New().SetFollow([]string{`^$`})
if _, err := s.Parse("https://example.com/sitemap-index.xml", nil); err != nil {
    log.Fatal(err)
}
for _, sm := range s.GetSitemaps() {
    fmt.Println(sm.Loc)
}

With the dates, the sitemaps that were modified since an earlier visit can be told from the rest, and parsed on their own. A sitemap without a date may have been modified as well:

for _, sm := range s.GetSitemaps() {
    if sm.LastMod == nil || sm.LastMod.After(lastVisit) {
        // parse sm.Loc
    }
}

See examples/sitemaps for a runnable example of the sitemaps a call lists, and examples/lastmod for one of their dates.

GetSitemapCount

Returns the number of sitemaps GetSitemaps() returns.

count := s.GetSitemapCount()
GetErrors

Returns the errors encountered during the most recent Parse() / ParseContext() call, together with any outstanding configuration errors recorded by the Set* methods (see Reusing an instance).

errs := s.GetErrors()

Errors are typed and can be inspected with errors.As:

Type When returned Useful fields
*ConfigError A Set* method received an invalid value Field (setting name), Err (root cause)
*NetworkError An HTTP fetch failed, or the response is larger than the size limit, see Max response size URL (requested URL), Err (root cause)
*ParseError A sitemap document could not be parsed (not a sitemap, empty, broken XML or gzip content, larger than the size limit) URL (sitemap URL; after a redirect, the URL the sitemap was served from), Err (root cause)
*ParseError The depth limit was reached, see Max depth URL (the sitemap index whose sitemaps were not followed), Err (root cause)
*ParseError The limit on the sitemaps, on the URLs or on the validation errors of the call was reached, see Max sitemaps, Max URLs and Max validation errors URL (the URL passed to Parse()), Err (root cause)
*ParseError The call was cut short by its context, see Parse with context URL (the URL passed to ParseContext()), Err (the error of the context)
*ValidationError A URL or field value failed validation URL (the rejected URL, or the page or sitemap the rejected value belongs to), Err (root cause)

All types implement Unwrap(), enabling errors.Is traversal to the root cause.

No more *ValidationError values are recorded than SetMaxValidationErrors() allows, see Max validation errors. The errors of the other types are all recorded.

The error that Parse() / ParseContext() return is one of these as well, see Parse.

The slice returned is the list the instance keeps, not a copy of it, like the one GetURLs() returns. It must not be modified: an error that is set or moved in it is set or moved for every other caller as well. Appending to it is safe, as the slice has no capacity to spare and append therefore copies it. It does not change afterwards: errors recorded later are in what a later GetErrors() call returns. For a list to sort or to change, see Errors().

for _, err := range s.GetErrors() {
    var netErr *sitemap.NetworkError
    if errors.As(err, &netErr) {
        fmt.Printf("fetch failed for %s: %v\n", netErr.URL, netErr.Err)
        continue
    }
    var valErr *sitemap.ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("validation error for %s: %v\n", valErr.URL, valErr.Err)
        continue
    }
}
Limit errors

The error that is recorded when a limit is reached wraps an error that stands for that limit, so errors.Is tells which limit it was, with no need to read the message:

Error Limit Wrapped by
ErrMaxResponseSize SetMaxResponseSize() the *NetworkError of a response over the limit, and the *ParseError of gzip content over it once decompressed
ErrMaxDepth SetMaxDepth() the *ParseError of a sitemap index whose sitemaps were not followed
ErrMaxSitemaps SetMaxSitemaps() the *ParseError of the call
ErrMaxURLs SetMaxURLs() the *ParseError of the call
ErrMaxValidationErrors SetMaxValidationErrors() the *ParseError of the call
for _, err := range s.GetErrors() {
    switch {
    case errors.Is(err, sitemap.ErrMaxURLs):
        fmt.Println("the URLs are not all there:", err)
    case errors.Is(err, sitemap.ErrMaxSitemaps):
        fmt.Println("the sitemaps were not all fetched or listed:", err)
    }
}

errors.Is works on the error Parse() / ParseContext() return as well, which is about a limit when the document at the URL they are called for is over the size limit. The messages of the errors are what they were before these errors were introduced.

See examples/errors for a runnable example.

Errors

Returns an iterator over the errors GetErrors() returns, in the same order:

for err := range s.Errors() {
    log.Print(err)
}

The errors are the values the instance keeps, the very error Parse() / ParseContext() returned among them, so they can be compared with it, and inspected with errors.As and errors.Is like the ones GetErrors() returns. The list is not handed out: slices.Collect(s.Errors()) returns a list of the caller's own, which may be sorted and changed.

Like a loop over URLs(), a loop goes through the errors as they are when it starts, and no lock is held while it runs.

See examples/iterators for a runnable example.

GetErrorsCount

Returns the number of errors GetErrors() returns.

errCount := s.GetErrorsCount()

Examples

Examples can be found in /examples: programs to be run with go run, one in a directory.

Shorter ones are part of the package documentation, where each is shown with its output next to the function, the method or the type it is an example of. They are the Example functions of example_test.go, which go test runs, and none of them needs a network:

go test -run '^Example' -v .

Documentation

Overview

Package sitemap fetches and parses sitemaps and collects the URLs they list.

A call to S.Parse or S.ParseContext takes the URL of a robots.txt, of a sitemap index or of a sitemap. It follows the sitemaps a robots.txt names and the ones a sitemap index lists, and collects the pages of every sitemap it reaches:

s := sitemap.New()
if _, err := s.Parse("https://www.sitemaps.org/sitemap.xml", nil); err != nil {
	log.Fatal(err)
}
for _, u := range s.GetURLs() {
	fmt.Println(u.Loc)
}

The document at the URL can be handed to the call in place of nil. It is not fetched then; the sitemaps it lists are.

The sitemaps the call found on its way, the ones a robots.txt names and a sitemap index lists, are returned by S.GetSitemaps, whether or not they were fetched, and with the date a sitemap index gives for them, see Sitemap.

S.GetURLs returns the list the instance keeps, which is to be read and not to be changed. S.URLs is an iterator over the same URLs that yields each of them as a copy of the caller's own, and S.Errors is one over the errors S.GetErrors returns. A list of the URLs to sort or to change is collected from the iterator:

urls := slices.Collect(s.URLs())
slices.SortFunc(urls, func(a, b sitemap.URL) int {
	return strings.Compare(a.Loc, b.Loc)
})

Formats

A sitemap is a <urlset> of the sitemaps.org protocol, an RSS 2.0 feed, an Atom 1.0 feed, or a plain text file with a URL on a line. A sitemap and a sitemap index may be gzip compressed. An entry of a <urlset> is returned with its <lastmod>, <changefreq> and <priority>, and with what the image, news, video and hreflang extensions say about the page, see URL. Of a feed, the location of a page and the date it was last modified are taken: the <pubDate> of an RSS item and the <updated> of an Atom entry. A text file gives nothing but the location.

Configuration

An instance is configured with its Set methods, which can be chained:

s := sitemap.New().
	SetUserAgent("my-crawler/1.0").
	SetFetchTimeout(10).
	SetFollow([]string{`^https://example\.com/sitemaps/`})

A Set method that is called with a value it does not accept records a ConfigError. Nothing is parsed while one is outstanding; calling the method again with a valid value clears it.

The parser is in tolerant mode unless S.SetStrict says otherwise. Tolerant mode makes the best of a document that does not keep to the protocol: a relative URL, for one, is resolved against the URL of the sitemap. Strict mode holds a document to the protocol, and skips and reports the entries that do not keep to it.

What a call may take up is bounded: the size of a response by S.SetMaxResponseSize, the depth of nested sitemap indexes by S.SetMaxDepth, the number of sitemaps it fetches and of sitemaps it lists by S.SetMaxSitemaps, the number of URLs it collects by S.SetMaxURLs, the number of validation errors it records by S.SetMaxValidationErrors, and the sitemaps it works on at a time by S.SetMaxConcurrency. S.SetFollow restricts the sitemaps that are fetched, S.SetRules the URLs that are collected.

Errors

Parse and ParseContext return an error when the call fails for the URL it was made for: the URL is not valid, or the document at it cannot be fetched or parsed. What goes wrong further on does not fail the call and is reported by S.GetErrors only: a sitemap that cannot be fetched or parsed, an entry that is not valid, a limit that is reached. A nil error therefore does not mean that nothing was skipped.

The errors are of the types ConfigError, NetworkError, ParseError and ValidationError, to be told apart with errors.As.

The error of a limit that was reached wraps the error that stands for the limit, so errors.Is tells which limit it was: ErrMaxResponseSize, ErrMaxDepth, ErrMaxSitemaps, ErrMaxURLs or ErrMaxValidationErrors.

for _, err := range s.GetErrors() {
	if errors.Is(err, sitemap.ErrMaxURLs) {
		log.Print("the URLs are not all there: ", err)
	}
}

Concurrency

The methods of an instance may be called from several goroutines. An instance carries out one Parse or ParseContext call at a time, a second one waits for the first to end; use an instance for each of the calls that are to run side by side. An instance can be used for any number of calls one after the other: its results are those of the most recent one.

Example

A document that is handed to the call is parsed as the one at the URL, without being fetched. With nil in its place, the document is fetched, see the example of Parse.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc><lastmod>2024-03-01</lastmod></url>
  <url><loc>https://example.com/about</loc></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	fmt.Println(s.GetURLCount(), "URLs")
	for _, u := range s.GetURLs() {
		fmt.Println(u.Loc)
	}
}
Output:
2 URLs
https://example.com/
https://example.com/about

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMaxResponseSize is wrapped by the error of a document that is larger than the limit
	// set with SetMaxResponseSize: the *NetworkError of a response that exceeds it, and the
	// *ParseError of gzip content that exceeds it once it is decompressed.
	ErrMaxResponseSize = errors.New("sitemap: maximum response size exceeded")

	// ErrMaxDepth is wrapped by the *ParseError of a sitemap index whose sitemaps were not
	// followed because the limit set with SetMaxDepth is reached.
	ErrMaxDepth = errors.New("sitemap: maximum depth reached")

	// ErrMaxSitemaps is wrapped by the *ParseError which tells that the limit set with
	// SetMaxSitemaps kept the call from fetching or from listing a sitemap.
	ErrMaxSitemaps = errors.New("sitemap: maximum number of sitemaps reached")

	// ErrMaxURLs is wrapped by the *ParseError which tells that the limit set with SetMaxURLs
	// kept the call from collecting a URL.
	ErrMaxURLs = errors.New("sitemap: maximum number of URLs reached")

	// ErrMaxValidationErrors is wrapped by the *ParseError which tells that the limit set with
	// SetMaxValidationErrors kept the call from recording a *ValidationError.
	ErrMaxValidationErrors = errors.New("sitemap: maximum number of validation errors reached")
)

The errors below stand for the limits of a Parse or ParseContext call. The error that is recorded when a limit is reached wraps the one of that limit, so errors.Is tells which limit it was without the message of the error being read:

for _, err := range s.GetErrors() {
    if errors.Is(err, sitemap.ErrMaxURLs) {
        fmt.Println("the URLs are not all there:", err)
    }
}

The message of the recorded error tells the limit and its value, as in `parse "https://example.com/sitemap.xml": limit of 100000 URLs reached`.

Functions

This section is empty.

Types

type AlternateLink struct {
	// Rel is the relation of the link to the page, the value of the rel attribute:
	// "alternate" for another version of the page. Strict mode leaves out and reports
	// a link with any other value, an empty one included.
	Rel string `xml:"rel,attr"`
	// Hreflang names the language of the version, and the region it is for if it is
	// for one, the value of the hreflang attribute: "de", "de-CH", or "x-default" for
	// the version of no language in particular. Strict mode leaves out and reports a
	// link without one.
	Hreflang string `xml:"hreflang,attr"`
	// Href is the URL of the version, the value of the href attribute. It is never
	// empty, and it may be on another host than the page. Strict mode requires an
	// absolute HTTP or HTTPS URL: a link whose Href is not one is left out and
	// reported.
	Href string `xml:"href,attr"`
}

AlternateLink represents an alternate version of a page (hreflang) per the XHTML standard used in sitemaps. Reference: https://developers.google.com/search/docs/specialty/international/localized-versions#sitemap

A link is known by its Href, and one without an Href is not among the links of a URL.

type ConfigError added in v0.9.0

type ConfigError struct {
	// Field is the configuration field name (e.g. "maxDepth", "follow", "rules").
	Field string
	// Err is the underlying validation error.
	Err error
}

ConfigError is returned when a configuration setter receives an invalid value. Callers can inspect the Field to determine which configuration setting caused the error.

Example usage:

var cfgErr *sitemap.ConfigError
if errors.As(err, &cfgErr) {
    fmt.Println("bad config field:", cfgErr.Field)
}
Example

A Set method that is given a value it does not accept records an error. Nothing is parsed while such an error is outstanding; setting a valid value clears it.

package main

import (
	"errors"
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	s := sitemap.New().SetMaxDepth(0)

	for _, err := range s.GetErrors() {
		var configErr *sitemap.ConfigError
		if errors.As(err, &configErr) {
			fmt.Println(configErr.Field, "-", configErr.Err)
		}
	}

	content := `<urlset><url><loc>https://example.com/</loc></url></urlset>`
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		fmt.Println(err)
	}

	s.SetMaxDepth(5)
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}
	fmt.Println(s.GetURLCount(), "URL")
}
Output:
maxDepth - must be greater than 0, got 0
errors occurred before parsing, see GetErrors() for details
1 URL

func (*ConfigError) Error added in v0.9.0

func (e *ConfigError) Error() string

Error returns the message of the error, which names the configuration field and tells what is wrong with the value it was given: `config "maxDepth": must be greater than 0, got 0`.

func (*ConfigError) Unwrap added in v0.9.0

func (e *ConfigError) Unwrap() error

Unwrap returns the underlying error, enabling errors.Is / errors.As traversal.

type Image added in v0.7.0

type Image struct {
	// Loc is the URL of the image, the content of <image:loc>. It is never empty, and it
	// may be on another host than the page. Strict mode requires an absolute HTTP or
	// HTTPS URL: an image whose Loc is not one is left out and reported.
	Loc string `xml:"http://www.google.com/schemas/sitemap-image/1.1 loc"`
	// Title is the title of the image, the content of <image:title>.
	Title string `xml:"http://www.google.com/schemas/sitemap-image/1.1 title"`
	// Caption is the caption of the image, the content of <image:caption>.
	Caption string `xml:"http://www.google.com/schemas/sitemap-image/1.1 caption"`
	// GeoLocation is the place the image shows, the content of <image:geo_location>,
	// such as "Limerick, Ireland".
	GeoLocation string `xml:"http://www.google.com/schemas/sitemap-image/1.1 geo_location"`
	// License is the URL of the licence of the image, the content of <image:license>.
	// Strict mode reports one that is no absolute HTTP or HTTPS URL; the image is
	// returned all the same, with the value as the document gives it.
	License string `xml:"http://www.google.com/schemas/sitemap-image/1.1 license"`
}

Image is a structure of <image:image> in <url>, per the Google Image Sitemap extension. Reference: https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps

An image is known by its Loc, and one without a Loc is not among the images of a URL. Every other field is empty when the image has no element for it.

Example

The images of a page are those of its entry that have a location.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
  <url>
    <loc>https://example.com/gallery</loc>
    <image:image>
      <image:loc>https://cdn.example.net/harbour.jpg</image:loc>
      <image:title>The harbour at dawn</image:title>
    </image:image>
    <image:image>
      <image:loc>https://cdn.example.net/lighthouse.jpg</image:loc>
    </image:image>
  </url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		for _, image := range u.Images {
			fmt.Printf("%s %q\n", image.Loc, image.Title)
		}
	}
}
Output:
https://cdn.example.net/harbour.jpg "The harbour at dawn"
https://cdn.example.net/lighthouse.jpg ""

type LastModTime added in v1.0.0

type LastModTime struct {
	// Time is the date and time the element gives. Where the element gives a day and no
	// time, the time is midnight in UTC; where it gives a month or a year and no more,
	// the date is the first day of that month or of that year.
	time.Time
}

LastModTime is the date, or the date and time, that an element of a document holds: a <lastmod> of a sitemap or of a sitemap index, a date element of an extension, the <pubDate> of an RSS item or the <updated> of an Atom entry. It embeds time.Time.

In the URLs and the sitemaps that Parse and ParseContext collect, a field of this type is nil when the element is absent, when it is empty, and when what it holds cannot be parsed. A field that is set therefore holds the date the document gives.

Example

A date is given as a year, a month, a day, or a day and a time with its time zone. LastModTime embeds time.Time, whose methods it therefore has.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/year</loc><lastmod>2024</lastmod></url>
  <url><loc>https://example.com/day</loc><lastmod>2024-03-01</lastmod></url>
  <url><loc>https://example.com/time</loc><lastmod>2024-03-01T10:30:00+01:00</lastmod></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	since := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC)
	for _, u := range s.GetURLs() {
		fmt.Println(u.LastMod.Format(time.RFC3339), u.LastMod.After(since))
	}
}
Output:
2024-01-01T00:00:00Z false
2024-03-01T00:00:00Z true
2024-03-01T10:30:00+01:00 true

func (*LastModTime) UnmarshalXML added in v1.0.0

func (l *LastModTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

UnmarshalXML implements xml.Unmarshaler, for code that decodes sitemap XML with encoding/xml itself. It accepts a date or a date and time in one of the W3C formats the sitemaps.org protocol allows, and fails for anything else.

An element that is empty, or holds only whitespace, is not an error and leaves l as it is. Note that encoding/xml allocates a pointer field for every element it meets, so a *LastModTime field decoded this way is not nil for an empty element: it holds the zero time, which IsZero tells. Parse and ParseContext do not read dates this way; in the URLs they collect, the field of an empty element is nil.

type NetworkError added in v0.9.0

type NetworkError struct {
	// URL is the URL that was being fetched when the error occurred.
	URL string
	// Err is the underlying network or HTTP error.
	Err error
}

NetworkError is returned when an HTTP fetch fails. Callers can inspect URL to determine which resource could not be retrieved.

A response that is larger than the limit set with SetMaxResponseSize is such a failure: Err wraps ErrMaxResponseSize then, which errors.Is matches.

Example usage:

var netErr *sitemap.NetworkError
if errors.As(err, &netErr) {
    fmt.Println("failed to fetch:", netErr.URL)
}
Example

A document that cannot be fetched is reported by a NetworkError. It fails the call when it is the document the call was made for.

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	server := httptest.NewServer(http.NotFoundHandler())
	defer server.Close()

	s := sitemap.New()
	_, err := s.Parse(server.URL+"/sitemap.xml", nil)

	var networkErr *sitemap.NetworkError
	if errors.As(err, &networkErr) {
		fmt.Println(strings.TrimPrefix(networkErr.URL, server.URL))
		fmt.Println(networkErr.Err)
	}
}
Output:
/sitemap.xml
received HTTP status 404

func (*NetworkError) Error added in v0.9.0

func (e *NetworkError) Error() string

Error returns the message of the error, which names the URL and tells why it could not be fetched: `fetch "https://example.com/sitemap.xml": received HTTP status 404`.

func (*NetworkError) Unwrap added in v0.9.0

func (e *NetworkError) Unwrap() error

Unwrap returns the underlying error, enabling errors.Is / errors.As traversal.

type News added in v0.7.0

type News struct {
	// Publication is the publication the article appears in, the <news:publication> of
	// the entry.
	Publication NewsPublication `xml:"http://www.google.com/schemas/sitemap-news/0.9 publication"`
	// PublicationDate is the date the article was published, the content of
	// <news:publication_date>. It is nil if the element is absent, if it is empty, and
	// if what it holds cannot be parsed, which is reported in both modes. Strict mode
	// reports news without one.
	PublicationDate *LastModTime `xml:"http://www.google.com/schemas/sitemap-news/0.9 publication_date"`
	// Title is the title of the article, the content of <news:title>. It is empty if
	// the element is absent. Strict mode reports news without one.
	Title string `xml:"http://www.google.com/schemas/sitemap-news/0.9 title"`
}

News is a structure of <news:news> in <url>, per the Google News Sitemap extension. Reference: https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap

Strict mode reports each of the fields the extension requires and the entry lacks: the name and the language of the publication, the publication date and the title. The news is returned all the same, with what the entry does give.

Example

The news of a page is nil unless its entry has a <news:news> element.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
  <url>
    <loc>https://example.com/news/harbour-reopens</loc>
    <news:news>
      <news:publication>
        <news:name>The Example Times</news:name>
        <news:language>en</news:language>
      </news:publication>
      <news:publication_date>2024-03-01</news:publication_date>
      <news:title>The harbour reopens</news:title>
    </news:news>
  </url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		if u.News == nil {
			continue
		}
		fmt.Println(u.News.Title)
		fmt.Println(u.News.Publication.Name, u.News.Publication.Language)
		fmt.Println(u.News.PublicationDate.Format(time.DateOnly))
	}
}
Output:
The harbour reopens
The Example Times en
2024-03-01

type NewsPublication added in v0.7.0

type NewsPublication struct {
	// Name is the name of the publication, the content of <news:name>. It is empty if
	// the element is absent. Strict mode reports news without one.
	Name string `xml:"http://www.google.com/schemas/sitemap-news/0.9 name"`
	// Language is the language of the publication, the content of <news:language>: a
	// language code such as "en". It is empty if the element is absent. Strict mode
	// reports news without one.
	Language string `xml:"http://www.google.com/schemas/sitemap-news/0.9 language"`
}

NewsPublication is a structure of <news:publication> in <news:news>.

type ParseError added in v0.9.0

type ParseError struct {
	// URL is the sitemap URL that was being parsed when the error occurred. For a sitemap
	// reached through a redirect it is the URL the sitemap was served from.
	// When the depth limit is reached, it is the sitemap index whose sitemaps were not
	// followed. When the limit on the sitemaps, on the URLs or on the validation errors of a
	// call is reached, or the call was cut short, it is the URL the call was made for.
	URL string
	// Err is the underlying parse error.
	Err error
}

ParseError is returned when XML or gzip parsing of a sitemap document fails. Callers can inspect URL to determine which sitemap could not be parsed.

It is also the error of a parse that was not carried through for another reason: the sitemaps a sitemap index lists were not followed because the depth limit is reached, sitemaps, URLs or validation errors were left out because the limit on the sitemaps, on the URLs or on the validation errors of a call is reached, or the call was cut short because its context is done. In the latter case Err is the error of the context, so errors.Is matches context.Canceled and context.DeadlineExceeded. When a limit is reached, Err wraps the error that stands for the limit, which errors.Is matches as well: ErrMaxDepth, ErrMaxSitemaps, ErrMaxURLs or ErrMaxValidationErrors, and ErrMaxResponseSize for gzip content that is too large.

Example usage:

var parseErr *sitemap.ParseError
if errors.As(err, &parseErr) {
    fmt.Println("failed to parse sitemap:", parseErr.URL)
}
Example

A document that cannot be parsed is reported by a ParseError.

package main

import (
	"errors"
	"fmt"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<html><body>Not found</body></html>`

	s := sitemap.New()
	_, err := s.Parse("https://example.com/sitemap.xml", &content)

	var parseErr *sitemap.ParseError
	if errors.As(err, &parseErr) {
		fmt.Println(parseErr.URL)
		fmt.Println(parseErr.Err)
	}
}
Output:
https://example.com/sitemap.xml
unrecognized sitemap format (root element: "html")

func (*ParseError) Error added in v0.9.0

func (e *ParseError) Error() string

Error returns the message of the error, which names the URL and tells why the parse failed or was not carried through: `parse "https://example.com/sitemap.xml": sitemap content is empty`.

func (*ParseError) Unwrap added in v0.9.0

func (e *ParseError) Unwrap() error

Unwrap returns the underlying error, enabling errors.Is / errors.As traversal.

type S

type S struct {
	// contains filtered or unexported fields
}

S is a sitemap parser. An instance holds the configuration of the parser, which its Set methods change and its Get methods of the same names return, and the results of its most recent Parse or ParseContext call: the URLs the call collected, the sitemaps it found on its way and the errors it recorded, see GetURLs, GetSitemaps and GetErrors.

An instance is made with New, which gives the configuration its defaults. The zero value has none of them and is not meant to be used.

The methods of an instance may be called from several goroutines. An instance carries out one call at a time, and can be used for any number of calls one after the other.

func New

func New() *S

New creates a new instance of the S structure. It initializes the structure with default configuration values and returns a pointer to the created instance.

Example

The Set methods return the instance, so they can be chained.

package main

import (
	"fmt"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	s := sitemap.New().
		SetUserAgent("my-crawler/1.0").
		SetFetchTimeout(10).
		SetMaxDepth(3)

	fmt.Println(s.GetUserAgent())
	fmt.Println(s.GetFetchTimeout())
	fmt.Println(s.GetMaxDepth())
	fmt.Println(s.GetStrict())
}
Output:
my-crawler/1.0
10
3
false

func (*S) Errors added in v1.3.0

func (s *S) Errors() iter.Seq[error]

Errors returns an iterator over the errors GetErrors returns, in the same order:

for err := range s.Errors() {
	log.Print(err)
}

It goes through the errors as they are when the loop starts. Errors that are recorded while the loop runs are not among them, and a Parse or ParseContext call that starts in the meantime does not disturb it. No lock is held while the loop runs, so the loop may call every method of s. The iterator can be used for any number of loops, each of which starts anew.

The errors are the values the instance keeps, the very error Parse or ParseContext returned among them, so that they can be compared with it. The list is not handed out: nothing a loop does changes which errors the instance returns, or their order. The iterator yields nothing for a nil *S.

Example

The iterator yields the errors GetErrors returns.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>ftp://example.com/files</loc></url>
  <url><loc>mailto:someone@example.com</loc></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for err := range s.Errors() {
		fmt.Println(err)
	}
}
Output:
validate "ftp://example.com/files": unsupported scheme "ftp"
validate "mailto:someone@example.com": unsupported scheme "mailto"

func (*S) GetErrors

func (s *S) GetErrors() []error

GetErrors returns the errors of the most recent Parse or ParseContext call, together with the configuration errors that are outstanding: those recorded by a Set method that was called with a value it does not accept, and not again with a valid one since. The errors of a call are discarded when the next call starts; configuration errors are kept until the setting is corrected.

Each error is a *ConfigError, a *NetworkError, a *ParseError or a *ValidationError, to be told apart with errors.As. They are in the order they were recorded in. With multi-threading on, see SetMultiThread, that order is not the same from one call to the next for the errors of different sitemaps.

An error that tells a limit of the call was reached wraps the error that stands for the limit, to be told with errors.Is: ErrMaxResponseSize, ErrMaxDepth, ErrMaxSitemaps, ErrMaxURLs or ErrMaxValidationErrors.

No more *ValidationError values are among them than the limit set with SetMaxValidationErrors allows. If a call had more to record, it left the later ones out and recorded the error of that limit.

The error Parse or ParseContext returned is among them, unless it tells that configuration errors are outstanding: then the configuration errors are. A call that returns a nil error may have recorded errors as well, about sitemaps and entries it had to skip and about limits it reached.

While a call is running, GetErrors returns the errors recorded so far; those recorded afterwards are not added to the slice it returned.

The slice that is returned is the list the instance keeps, not a copy of it. It must not be modified: an error that is set or moved in it is set or moved for every other caller as well. Appending to it is safe, as the slice has no capacity to spare and append therefore copies it. Errors goes through the same errors without handing the list out, and slices.Collect(s.Errors()) returns a list the caller may sort or change. The slice is nil for a nil *S.

Example

The errors of a call are of four types, which errors.As tells apart. An entry or a value that is not valid does not fail the call: it is left out, and reported here.

package main

import (
	"errors"
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
  <url><loc>ftp://example.com/files</loc></url>
  <url><loc>https://example.com/about</loc><lastmod>yesterday</lastmod></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	fmt.Println(s.GetURLCount(), "URLs,", s.GetErrorsCount(), "errors")
	for _, err := range s.GetErrors() {
		var validationErr *sitemap.ValidationError
		if errors.As(err, &validationErr) {
			fmt.Printf("%s: %v\n", validationErr.URL, validationErr.Err)
		}
	}
}
Output:
2 URLs, 2 errors
ftp://example.com/files: unsupported scheme "ftp"
https://example.com/about: invalid <lastmod> value "yesterday"

func (*S) GetErrorsCount

func (s *S) GetErrorsCount() int64

GetErrorsCount returns the number of errors GetErrors returns. It returns 0 for a nil *S.

func (*S) GetFetchTimeout added in v1.0.0

func (s *S) GetFetchTimeout() uint16

GetFetchTimeout returns the current fetch timeout in seconds.

func (*S) GetFollow added in v1.0.0

func (s *S) GetFollow() []string

GetFollow returns a copy of the current follow regex pattern strings.

func (*S) GetHTTPClient added in v1.0.0

func (s *S) GetHTTPClient() *http.Client

GetHTTPClient returns the custom HTTP client, or nil if the default client behaviour is used.

func (*S) GetMaxConcurrency added in v1.0.0

func (s *S) GetMaxConcurrency() int

GetMaxConcurrency returns the maximum number of sitemaps fetched or parsed at the same time. A value of 0 means unlimited concurrency.

func (*S) GetMaxDepth added in v1.0.0

func (s *S) GetMaxDepth() int

GetMaxDepth returns the maximum recursion depth for following sitemap indexes.

func (*S) GetMaxResponseSize added in v1.0.0

func (s *S) GetMaxResponseSize() int64

GetMaxResponseSize returns the maximum allowed HTTP response size in bytes.

func (*S) GetMaxSitemaps added in v1.2.0

func (s *S) GetMaxSitemaps() int

GetMaxSitemaps returns the maximum number of sitemaps a Parse or ParseContext call fetches, which is the maximum number of sitemaps it lists as well, see SetMaxSitemaps. A value of 0 means no limit.

func (*S) GetMaxURLs added in v1.2.0

func (s *S) GetMaxURLs() int

GetMaxURLs returns the maximum number of URLs a Parse or ParseContext call collects. A value of 0 means no limit.

func (*S) GetMaxValidationErrors added in v1.3.0

func (s *S) GetMaxValidationErrors() int

GetMaxValidationErrors returns the maximum number of validation errors a Parse or ParseContext call records, see SetMaxValidationErrors. A value of 0 means no limit.

func (*S) GetMultiThread added in v1.0.0

func (s *S) GetMultiThread() bool

GetMultiThread returns whether multi-threaded fetching and parsing is enabled.

func (*S) GetRandomURLs

func (s *S) GetRandomURLs(n int) []URL

GetRandomURLs returns a slice of randomly selected URLs from the S object's URL list. The number of URLs to select is specified by the parameter n. If n exceeds the number of URLs in the list, all of them are returned, in random order. If n is zero or negative, or the S object is nil, an empty slice is returned. The function creates a copy of the original URLs list and randomly selects n URLs from it, removing them to avoid duplicates. The selected URLs are returned as a new slice. Each of them is a copy that is the caller's own, with everything it points to, like the ones URLs yields.

Example

No URL is returned twice, and no more of them than there are.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/1</loc></url>
  <url><loc>https://example.com/2</loc></url>
  <url><loc>https://example.com/3</loc></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	fmt.Println(len(s.GetRandomURLs(2)))
	for _, u := range s.GetRandomURLs(5) {
		fmt.Println(u.Loc)
	}
}
Output:
2
https://example.com/1
https://example.com/2
https://example.com/3

func (*S) GetRules added in v1.0.0

func (s *S) GetRules() []string

GetRules returns a copy of the current URL filter regex pattern strings.

func (*S) GetSitemapCount added in v1.3.0

func (s *S) GetSitemapCount() int64

GetSitemapCount returns the number of sitemaps GetSitemaps returns. It returns 0 for a nil *S.

func (*S) GetSitemaps added in v1.3.0

func (s *S) GetSitemaps() []Sitemap

GetSitemaps returns the sitemaps that the documents of the most recent Parse or ParseContext call list: the entries of the sitemap indexes the call read, and the Sitemap lines of the robots.txt it was made for.

A sitemap is among them whether or not the call fetched it. One that the patterns set with SetFollow leave out is, and so is one that could not be fetched, and one that a limit of the call kept from being fetched. A location that is no URL to request is not: it is reported in GetErrors instead. Neither is the document the call was made for, unless a document of the call lists it.

Each sitemap is returned once, however many documents list it, and by the URL it is requested by, see Sitemap. The sitemaps of a document are in the order the document lists them. With multi-threading off, the documents are read one after the other, so the order is the same from one call to the next; with multi-threading on, see SetMultiThread, it is not for the sitemaps of different documents.

No more sitemaps are returned than the limit set with SetMaxSitemaps allows. If the documents list more, the first ones the call found are returned, and the error that tells the limit was reached is recorded. The sitemaps that are listed and the ones that are fetched are counted apart, so the ones returned then need not be the ones that were fetched.

A sitemap that a sitemap index lists comes with the date the index gives for it, see Sitemap.LastMod.

While a call is running, GetSitemaps returns the sitemaps found so far. The slice that is returned is a copy the caller may modify, the dates in it included. It is never nil, and empty for a nil *S.

Example

The sitemaps a sitemap index lists can be read without any of them being fetched, with a follow pattern that no URL matches.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://example.com/sitemap-pages.xml</loc><lastmod>2024-03-01</lastmod></sitemap>
  <sitemap><loc>https://example.com/sitemap-blog.xml</loc></sitemap>
</sitemapindex>`

	s := sitemap.New().SetFollow([]string{`^$`})
	if _, err := s.Parse("https://example.com/sitemap-index.xml", &content); err != nil {
		log.Fatal(err)
	}

	fmt.Println(s.GetSitemapCount(), "sitemaps")
	for _, listed := range s.GetSitemaps() {
		if listed.LastMod == nil {
			fmt.Println(listed.Loc, "(no date)")
			continue
		}
		fmt.Println(listed.Loc, listed.LastMod.Format(time.DateOnly))
	}
}
Output:
2 sitemaps
https://example.com/sitemap-pages.xml 2024-03-01
https://example.com/sitemap-blog.xml (no date)

func (*S) GetStrict added in v1.0.0

func (s *S) GetStrict() bool

GetStrict returns whether strict URL validation mode is enabled.

func (*S) GetURLCount

func (s *S) GetURLCount() int64

GetURLCount returns the count of URLs in the S struct.

func (*S) GetURLs

func (s *S) GetURLs() []URL

GetURLs returns the URLs of the most recent Parse or ParseContext call: the pages that the sitemaps it read list, less the ones the patterns set with SetRules leave out.

The URLs of a document are in the order the document lists them. With multi-threading off, the documents are read one after the other, so the order is the same from one call to the next; with multi-threading on, see SetMultiThread, it is not for the URLs of different documents. No more URLs are returned than the limit set with SetMaxURLs allows.

While a call is running, GetURLs returns the URLs collected so far; those collected afterwards are not added to the slice it returned.

The slice that is returned is the list the instance keeps, not a copy of it, so a call costs the same however many URLs there are. It must not be modified: a URL that is set or moved in it, as sorting it does, or changed through one of its pointers, is changed for every other caller as well, and for GetRandomURLs and URLs. Appending to it is safe, as the slice has no capacity to spare and append therefore copies it. URLs goes through the same URLs as copies, and slices.Collect(s.URLs()) returns a list the caller may sort or change. The slice is never nil, and empty for a nil *S.

func (*S) GetUserAgent added in v1.0.0

func (s *S) GetUserAgent() string

GetUserAgent returns the current user agent string used for HTTP requests.

func (*S) Parse

func (s *S) Parse(url string, urlContent *string) (*S, error)

Parse is a method of the S structure. It parses the given URL and its content.

Parse is a backward-compatible wrapper around ParseContext that uses context.Background(). For new code, prefer ParseContext so that callers can propagate cancellation and deadlines.

An instance can be reused: every call first discards the URLs, sitemaps and errors collected by the previous call. Configuration errors recorded by the Set* methods are the exception. They persist across calls, and while any of them is outstanding Parse does not parse anything and returns an error with the message "errors occurred before parsing, see GetErrors() for details". Calling the same setter again with a valid value clears its error.

A sitemap that is reached through a redirect is located at the URL it was served from, not at the URL that was requested: relative URLs in it are resolved against that URL, strict mode compares the URLs it lists with that URL, and the errors about the document name it.

It sets the mainURL field to the given URL. The content is the given URL content or, if that is nil, what the URL serves. If the URL is that of a robots.txt, it parses the robots.txt file and fetches URLs from the sitemap files mentioned in the robots.txt. A URL is that of a robots.txt when the last segment of its path is "robots.txt", in whichever letter case it is written and whether or not a query or a fragment follows it: "https://example.com/robots.txt?v=2" is one. If the URL is not that of a robots.txt, the content is checked and unzipped if necessary, then parsed and fetched. The content of a document is not kept once the document is parsed.

A call fetches no more sitemaps than SetMaxSitemaps allows, and collects no more URLs than SetMaxURLs allows, so that it comes to an end whatever the documents list. It records no more validation errors than SetMaxValidationErrors allows.

The sitemaps the documents of the call list are returned by GetSitemaps, whether or not they were fetched.

It returns the S structure, and a nil error if the document at the given URL was fetched and parsed. Otherwise the error tells why it was not:

  • a configuration error is outstanding, see above;
  • the URL is not valid: a *ValidationError;
  • the document could not be fetched: a *NetworkError;
  • the document could not be parsed: a *ParseError.

Except for the first, the error returned is the one GetErrors() holds about it. What goes wrong further on is in GetErrors() only and does not fail the call: a sitemap the document lists that cannot be fetched or parsed, a limit that is reached, an entry that is not valid. The error of a limit wraps the error that stands for it, such as ErrMaxURLs, so errors.Is tells which limit was reached.

Example

A call follows the sitemaps the document at its URL lists: here a robots.txt names a sitemap index, which lists two sitemaps. The site is served by a local test server, and "{site}" stands for its address.

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	documents := map[string]string{
		"/robots.txt": "User-agent: *\nSitemap: {site}/sitemap-index.xml\n",
		"/sitemap-index.xml": `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>{site}/sitemap-pages.xml</loc></sitemap>
  <sitemap><loc>{site}/sitemap-blog.xml</loc></sitemap>
</sitemapindex>`,
		"/sitemap-pages.xml": `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>{site}/</loc></url>
  <url><loc>{site}/about</loc></url>
</urlset>`,
		"/sitemap-blog.xml": `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>{site}/blog/first-post</loc></url>
</urlset>`,
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, strings.ReplaceAll(documents[r.URL.Path], "{site}", "http://"+r.Host))
	}))
	defer server.Close()

	// The sitemaps are fetched one at a time, so that their URLs are in the same order
	// every time.
	s := sitemap.New().SetMultiThread(false)
	if _, err := s.Parse(server.URL+"/robots.txt", nil); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println(strings.TrimPrefix(u.Loc, server.URL))
	}
}
Output:
/
/about
/blog/first-post
Example (Rss)

The pages of an RSS feed are its items. The date of a page is the <pubDate> of its item.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Example blog</title>
    <item>
      <link>https://example.com/blog/first-post</link>
      <pubDate>Mon, 15 Jan 2024 10:30:00 GMT</pubDate>
    </item>
    <item>
      <link>https://example.com/blog/second-post</link>
    </item>
  </channel>
</rss>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/feed.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		if u.LastMod == nil {
			fmt.Println(u.Loc, "(no date)")
			continue
		}
		fmt.Println(u.Loc, u.LastMod.Format(time.RFC3339))
	}
}
Output:
https://example.com/blog/first-post 2024-01-15T10:30:00Z
https://example.com/blog/second-post (no date)
Example (Text)

A text sitemap has a URL on a line. Empty lines and comments are skipped.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `# pages
https://example.com/
https://example.com/about

https://example.com/contact
`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.txt", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println(u.Loc)
	}
}
Output:
https://example.com/
https://example.com/about
https://example.com/contact

func (*S) ParseContext added in v0.4.0

func (s *S) ParseContext(ctx context.Context, url string, urlContent *string) (*S, error)

ParseContext parses the given URL and its content, honoring the supplied context for cancellation and deadlines.

The context is propagated through every HTTP request issued by the parser (both the initial fetch and the recursive sitemap-index/urlset fetches), so cancelling ctx aborts in-flight downloads and prevents new ones from starting. Already-parsed URLs accumulated in s.urls before cancellation remain available via GetURLs().

If ctx is done by the time the call ends, ParseContext returns a *ParseError that names the given URL and wraps the error of ctx, so errors.Is matches context.Canceled and context.DeadlineExceeded. The same error is recorded in the error list, once for the call: whether the sitemaps are fetched concurrently or one at a time, and however many of them were not fetched. A request that was cut short is recorded as the *NetworkError of that sitemap in addition.

A call that fails for the given URL itself, as described at Parse, returns and records that error alone. This is the case when it is the request for the given URL that is cut short: the error is the *NetworkError of that request, which wraps the error of ctx as well.

All other semantics match Parse.

Example

A call is cut short when its context is done. The error it returns then wraps the error of the context, so errors.Is matches it.

package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	// The server does not answer before the request is given up.
	server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
		<-r.Context().Done()
	}))
	defer server.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	s := sitemap.New()
	_, err := s.ParseContext(ctx, server.URL+"/sitemap.xml", nil)

	fmt.Println(errors.Is(err, context.DeadlineExceeded))
}
Output:
true

func (*S) SetFetchTimeout

func (s *S) SetFetchTimeout(fetchTimeout uint16) *S

SetFetchTimeout sets the fetch timeout for the Sitemap Parser. The fetch timeout determines how long the parser will wait for an HTTP request to complete. It should be specified in seconds as a uint16 value and must be greater than 0. Invalid values are ignored and a *ConfigError is recorded; a later call with a valid value clears it. Note: when a custom HTTP client is set via SetHTTPClient, this value has no effect. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetFollow added in v0.1.3

func (s *S) SetFollow(regexes []string) *S

SetFollow sets the follow patterns using the provided list of regex strings and compiles them into regex objects. When patterns are set, only the sitemaps whose URL matches one of them are fetched, whether a sitemap index lists them or a robots.txt names them. The URL passed to Parse is always fetched. Patterns longer than maxRegexPatternLength characters are rejected with a *ConfigError. Any errors encountered during compilation are recorded as *ConfigError values. Each call replaces both the patterns and the errors recorded by the previous call. The function returns a pointer to the S structure to allow method chaining.

Example

Only the sitemaps whose URL matches a pattern are fetched. The others are still among the sitemaps the call lists.

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	documents := map[string]string{
		"/sitemap-index.xml": `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>{site}/sitemap-products.xml</loc></sitemap>
  <sitemap><loc>{site}/sitemap-blog.xml</loc></sitemap>
</sitemapindex>`,
		"/sitemap-products.xml": `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>{site}/products/1</loc></url>
</urlset>`,
		"/sitemap-blog.xml": `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>{site}/blog/first-post</loc></url>
</urlset>`,
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, strings.ReplaceAll(documents[r.URL.Path], "{site}", "http://"+r.Host))
	}))
	defer server.Close()

	s := sitemap.New().SetFollow([]string{`/sitemap-blog\.xml$`})
	if _, err := s.Parse(server.URL+"/sitemap-index.xml", nil); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println("URL:", strings.TrimPrefix(u.Loc, server.URL))
	}
	for _, listed := range s.GetSitemaps() {
		fmt.Println("sitemap:", strings.TrimPrefix(listed.Loc, server.URL))
	}
}
Output:
URL: /blog/first-post
sitemap: /sitemap-products.xml
sitemap: /sitemap-blog.xml

func (*S) SetHTTPClient added in v0.6.0

func (s *S) SetHTTPClient(client *http.Client) *S

SetHTTPClient sets a custom HTTP client for the Sitemap Parser. When set, the provided client is used for all HTTP requests instead of the internally created default client. This allows callers to configure custom transports, proxies, TLS settings, authentication, or timeout strategies. When a custom client is provided, SetFetchTimeout has no effect; the client's own Timeout field controls the request deadline. Pass nil to reset to the default client behaviour. The function returns a pointer to the S structure to allow method chaining.

Example

A client of the caller's own is used for every request of the instance. Here it is the client of a test server that speaks HTTPS, which trusts the certificate of that server.

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `<urlset><url><loc>https://%s/page</loc></url></urlset>`, r.Host)
	}))
	defer server.Close()

	s := sitemap.New().SetHTTPClient(server.Client())
	if _, err := s.Parse(server.URL+"/sitemap.xml", nil); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println(strings.TrimPrefix(u.Loc, server.URL))
	}
}
Output:
/page

func (*S) SetMaxConcurrency added in v0.4.0

func (s *S) SetMaxConcurrency(maxConcurrency int) *S

SetMaxConcurrency sets the maximum number of sitemaps that are worked on at the same time when multi-threaded parsing is enabled. The default is 16. A value of 0 means unlimited concurrency. A positive value caps the number of sitemaps that are being fetched or parsed at any one time across the recursive sitemap-index traversal: a sitemap counts from the moment its request is sent until its content is parsed. That bounds the connections in use and the documents held in memory, which is recommended for very large sitemap indexes to avoid goroutine and connection blow-up. Negative values are rejected and a *ConfigError is recorded; a later call with a valid value clears it. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetMaxDepth added in v0.3.0

func (s *S) SetMaxDepth(maxDepth int) *S

SetMaxDepth sets the maximum recursion depth for following sitemap indexes. A sitemap index may reference other sitemap indexes; this limits how many levels deep the parser will follow. The default is 10. When the limit is reached, a *ParseError is recorded for every sitemap index whose sitemaps are not followed. It names that sitemap index and wraps ErrMaxDepth, which errors.Is matches. The value must be greater than 0; invalid values are ignored and a *ConfigError is recorded. A later call with a valid value clears it. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetMaxResponseSize added in v0.3.0

func (s *S) SetMaxResponseSize(maxResponseSize int64) *S

SetMaxResponseSize sets the maximum allowed HTTP response size in bytes. Responses exceeding this limit are rejected with a *NetworkError. The same limit caps the decompressed size of gzip-compressed content, of all the members of a gzip file together, whether it was fetched or supplied through the urlContent argument of Parse; content that expands beyond it is rejected with a *ParseError. Both errors wrap ErrMaxResponseSize, which errors.Is matches. The default is 50 MB, matching the sitemaps.org protocol limit. The value must be greater than 0; invalid values are ignored and a *ConfigError is recorded. A later call with a valid value clears it. Every value greater than 0 is a limit, math.MaxInt64 included: nothing can exceed that one, so it is the way to have no limit in effect. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetMaxSitemaps added in v1.2.0

func (s *S) SetMaxSitemaps(maxSitemaps int) *S

SetMaxSitemaps sets the maximum number of sitemaps a Parse or ParseContext call fetches. The default is 50,000, which is as many sitemaps as a sitemap index may list according to the sitemaps.org protocol. A value of 0 means no limit.

The sitemaps a robots.txt or a sitemap index lists count, on every level together: each one the call requests, whether or not the request succeeds. A sitemap that is listed more than once is requested and counted once. The document the call is made for does not count.

Once the limit is reached, the sitemaps that are left are not fetched, and a *ParseError naming the URL the call was made for is recorded, once for the call. It wraps ErrMaxSitemaps, which errors.Is matches. The call does not fail: what the sitemaps fetched until then yielded is returned. With multi-threading off, the sitemaps are fetched in the order they are listed, so it is the first ones that are fetched; with multi-threading on, which ones are depends on how fast the server answers. Nothing is recorded if no sitemap had to be left out.

The same limit is on the sitemaps GetSitemaps returns, which are the ones the documents list whether or not they are fetched: no more of them are kept than the limit allows, and the same error is recorded if one had to be left out of them. A call that fetches fewer sitemaps than it finds, as one with patterns set with SetFollow does, can therefore reach the limit by the sitemaps it finds.

The limit bounds the number of requests a document can make the parser send, and the memory the sitemaps of a call take up. Without it, a server can keep a call busy for as long as it likes by listing sitemaps that list ever further sitemaps.

Negative values are rejected and a *ConfigError is recorded; a later call with a valid value clears it. The value that applies to a call is the one set when the call starts. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetMaxURLs added in v1.2.0

func (s *S) SetMaxURLs(maxURLs int) *S

SetMaxURLs sets the maximum number of URLs a Parse or ParseContext call collects. The default is 10,000,000. A value of 0 means no limit.

The URLs that GetURLs returns count. An entry that is not valid does not, and neither does a URL that the patterns set with SetRules leave out.

Once the limit is reached, the call stops collecting: what is left of the document that reached the limit is left out unread, so it adds no errors either, the sitemaps that are left are not fetched, and a *ParseError naming the URL the call was made for is recorded, once for the call. It wraps ErrMaxURLs, which errors.Is matches. The call does not fail: the URLs collected until then are returned. With multi-threading off, they are the first ones in the order the sitemaps list them; with multi-threading on, which ones they are depends on how fast the server answers. Nothing is recorded if nothing had to be left out.

The limit bounds the memory the URLs of a call take up, whatever the documents list. The default is a last resort rather than a tight bound: ten million URLs take up gigabytes. Set a lower limit for documents that are not trusted.

Negative values are rejected and a *ConfigError is recorded; a later call with a valid value clears it. The value that applies to a call is the one set when the call starts. The function returns a pointer to the S structure to allow method chaining.

Example

A call collects no more URLs than the limit allows, and records that it had to leave some out. errors.Is tells the error of a limit from the others.

package main

import (
	"errors"
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/1</loc></url>
  <url><loc>https://example.com/2</loc></url>
  <url><loc>https://example.com/3</loc></url>
</urlset>`

	s := sitemap.New().SetMaxURLs(2)
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	fmt.Println(s.GetURLCount(), "URLs")
	for _, err := range s.GetErrors() {
		if errors.Is(err, sitemap.ErrMaxURLs) {
			fmt.Println(err)
		}
	}
}
Output:
2 URLs
parse "https://example.com/sitemap.xml": limit of 2 URLs reached

func (*S) SetMaxValidationErrors added in v1.3.0

func (s *S) SetMaxValidationErrors(maxValidationErrors int) *S

SetMaxValidationErrors sets the maximum number of validation errors a Parse or ParseContext call records. The default is 100,000. A value of 0 means no limit.

What is wrong with an entry of a document is recorded as a *ValidationError: a location that is no URL, a value that cannot be parsed, a field strict mode requires. How many entries of a document are not valid is up to the server, and such an entry does not count towards the limit set with SetMaxURLs. The errors of this type that GetErrors returns count, whatever they are about.

Once the limit is reached, the validation errors that are left are not recorded, and a *ParseError naming the URL the call was made for is recorded, once for the call. It wraps ErrMaxValidationErrors, which errors.Is matches. Nothing else changes: the documents are read to their end, and the URLs and the sitemaps of the call are the ones a call without the limit returns. With multi-threading off, the errors recorded are the first ones in the order the sitemaps list the entries; with multi-threading on, which ones they are depends on how fast the server answers. Nothing is recorded if no error had to be left out.

The errors of the other types do not count and are recorded whatever the limit is: a *NetworkError and a *ParseError are about a sitemap or about the call as a whole, so there is at most one of them for a sitemap the call fetches, and a few for the call. A call therefore still tells every sitemap it could not fetch or parse.

The limit bounds the memory the errors of a call take up, whatever the documents hold, together with the limit set with SetMaxSitemaps.

Negative values are rejected and a *ConfigError is recorded; a later call with a valid value clears it. The value that applies to a call is the one set when the call starts. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetMultiThread added in v0.1.2

func (s *S) SetMultiThread(multiThread bool) *S

SetMultiThread sets the multi-threading for the Sitemap Parser. The multi-threading flag determines whether the parser should fetch and parse the sitemaps concurrently using goroutines. When it is off, the sitemaps are fetched one at a time and in the order they are listed, those a robots.txt names as well as those of a sitemap index, and SetMaxConcurrency has no effect. The function returns a pointer to the S structure to allow method chaining.

func (*S) SetRules added in v0.1.3

func (s *S) SetRules(regexes []string) *S

SetRules sets the rules patterns using the provided list of regex strings and compiles them into regex objects. Patterns longer than maxRegexPatternLength characters are rejected with a *ConfigError. Any errors encountered during compilation are recorded as *ConfigError values. Each call replaces both the patterns and the errors recorded by the previous call. The function returns a pointer to the S structure to allow method chaining.

Example

Only the URLs that match a pattern are collected.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
  <url><loc>https://example.com/blog/first-post</loc></url>
  <url><loc>https://example.com/products/1</loc></url>
  <url><loc>https://example.com/blog/second-post</loc></url>
</urlset>`

	s := sitemap.New().SetRules([]string{`^https://example\.com/blog/`})
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println(u.Loc)
	}
}
Output:
https://example.com/blog/first-post
https://example.com/blog/second-post

func (*S) SetStrict added in v0.3.0

func (s *S) SetStrict(strict bool) *S

SetStrict enables or disables strict mode for URL validation. In strict mode, all URLs in sitemap <loc> elements must be absolute HTTP(S) URLs on the same host and protocol as the sitemap file, must hold neither a space, nor a control character, nor one of the characters " < > \ ^ ` { | }, which a URL has to give percent-encoded, and must not exceed 2048 characters, as required by the sitemaps.org specification. The host is compared without regard to letter case, and the default port of the protocol is the same as no port. In tolerant mode (default), relative URLs are resolved against the parent sitemap URL, and a space in a URL is percent-encoded. In both modes a URL has to name a host: one that names none is skipped and reported. The function returns a pointer to the S structure to allow method chaining.

Example

Tolerant mode, the default, makes the best of a sitemap that does not keep to the protocol. Strict mode skips and reports the entries that do not.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
  <url><loc>/about</loc></url>
  <url><loc>https://example.com/summer sale</loc></url>
  <url><loc>https://other.example/page</loc></url>
</urlset>`

	for _, strict := range []bool{false, true} {
		s := sitemap.New().SetStrict(strict)
		if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
			log.Fatal(err)
		}

		fmt.Println("strict:", strict)
		for _, u := range s.GetURLs() {
			fmt.Println("  URL:", u.Loc)
		}
		for _, err := range s.GetErrors() {
			fmt.Println("  error:", err)
		}
	}
}
Output:
strict: false
  URL: https://example.com/
  URL: https://example.com/about
  URL: https://example.com/summer%20sale
  URL: https://other.example/page
strict: true
  URL: https://example.com/
  error: validate "/about": strict mode: unsupported scheme ""
  error: validate "https://example.com/summer sale": strict mode: URL contains a space
  error: validate "https://other.example/page": strict mode: host "other.example" does not match sitemap host "example.com"

func (*S) SetUserAgent

func (s *S) SetUserAgent(userAgent string) *S

SetUserAgent sets the user agent for the Sitemap Parser. The user agent is used for making HTTP requests when parsing and fetching URLs. It should be a string representing the user agent header value. The function returns a pointer to the S structure to allow method chaining.

func (*S) URLs added in v1.3.0

func (s *S) URLs() iter.Seq[URL]

URLs returns an iterator over the URLs GetURLs returns, in the same order:

for u := range s.URLs() {
	fmt.Println(u.Loc)
}

Each URL is a copy that is the caller's own. Everything it points to is copied with it, its dates and values and what the extensions say about the page, so nothing that is done to it reaches the URLs the instance keeps. The list itself is not copied: a loop takes up the memory of one URL at a time, however many there are.

slices.Collect(s.URLs()) returns the URLs as a list of such copies, which the caller may sort and change. It cannot tell how long the list is going to be and grows it as it goes, which takes several times the memory of the list where the list is long. Giving the list its capacity spares that:

urls := slices.AppendSeq(make([]sitemap.URL, 0, s.GetURLCount()), s.URLs())

It goes through the URLs as they are when the loop starts. URLs that are collected while the loop runs are not among them, and a Parse or ParseContext call that starts in the meantime does not disturb it. No lock is held while the loop runs, so the loop may call every method of s. The iterator can be used for any number of loops, each of which starts anew. The iterator yields nothing for a nil *S.

Example

The iterator yields the URLs as copies, so they can be collected into a list that is the caller's own to sort, which the slice GetURLs returns is not.

package main

import (
	"fmt"
	"log"
	"slices"
	"strings"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/news</loc><priority>0.8</priority></url>
  <url><loc>https://example.com/about</loc><priority>0.3</priority></url>
  <url><loc>https://example.com/</loc><priority>1.0</priority></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for u := range s.URLs() {
		fmt.Println(u.Loc)
	}

	sorted := slices.SortedFunc(s.URLs(), func(a, b sitemap.URL) int {
		return strings.Compare(a.Loc, b.Loc)
	})
	fmt.Println("sorted:")
	for _, u := range sorted {
		fmt.Println(u.Loc)
	}
}
Output:
https://example.com/news
https://example.com/about
https://example.com/
sorted:
https://example.com/
https://example.com/about
https://example.com/news

type Sitemap added in v1.3.0

type Sitemap struct {
	// Loc is the URL the sitemap is requested by. It is never empty.
	Loc string
	// LastMod is the date the sitemap was last modified according to the sitemap index
	// that lists it. It is nil if no date is given, or none that can be parsed.
	LastMod *LastModTime
}

Sitemap is a sitemap that a document lists: an entry of a sitemap index, or a Sitemap line of a robots.txt. GetSitemaps returns the ones a call found.

Loc is the URL the sitemap is located at, the one it is requested by. It is an absolute URL that passed the checks a URL has to pass to be requested. In tolerant mode it can be requested as it is: a relative URL is resolved against the URL of the document that lists it, a space or a character that is not in ASCII is percent-encoded, and an internationalised host name is in its Punycode form. Strict mode returns it as the document gives it. A sitemap that is reached through a redirect is served from another URL than this one.

LastMod is the date the sitemap was last modified, as the sitemap index that lists it gives it: the <lastmod> of its entry. It is nil if the entry has none, if the element is empty, and if what it holds cannot be parsed, which is reported in GetErrors. It is nil for a sitemap that only a robots.txt names as well, which gives no date. If the documents of a call give more than one date for a sitemap, it is the latest of them.

Example

A sitemap that a sitemap index lists comes with the date the index gives for it, which tells the sitemaps that changed since an earlier visit from the rest.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://example.com/sitemap-2023.xml</loc><lastmod>2023-12-31</lastmod></sitemap>
  <sitemap><loc>https://example.com/sitemap-2024.xml</loc><lastmod>2024-03-01</lastmod></sitemap>
  <sitemap><loc>https://example.com/sitemap-pages.xml</loc></sitemap>
</sitemapindex>`

	// No sitemap is fetched: no URL matches the follow pattern.
	s := sitemap.New().SetFollow([]string{`^$`})
	if _, err := s.Parse("https://example.com/sitemap-index.xml", &content); err != nil {
		log.Fatal(err)
	}

	lastVisit := time.Date(2024, time.January, 15, 0, 0, 0, 0, time.UTC)
	for _, listed := range s.GetSitemaps() {
		// A sitemap without a date may have changed as well.
		if listed.LastMod == nil || listed.LastMod.After(lastVisit) {
			fmt.Println(listed.Loc)
		}
	}
}
Output:
https://example.com/sitemap-2024.xml
https://example.com/sitemap-pages.xml

type URL

type URL struct {
	// Loc is the URL of the page: the content of the <loc> of a <url>, the <link> of an
	// RSS item, the link of an Atom entry, or a line of a text sitemap. It is never
	// empty: an entry without a location is reported, not returned.
	Loc string `xml:"loc"`
	// LastMod is the date the page was last modified. It is nil if the entry gives no
	// date, if the element is empty, and if what it holds cannot be parsed, which is
	// reported. Strict mode skips a <url> whose <lastmod> cannot be parsed.
	LastMod *LastModTime `xml:"lastmod"`
	// ChangeFreq tells how often the page is likely to change, the content of the
	// <changefreq> of a <url>. It is nil if the element is absent or empty. A value
	// that is set is to be compared with the ChangeFreq constants, see URLChangeFreq
	// for what the two modes make of a value that is none of them.
	ChangeFreq *URLChangeFreq `xml:"changefreq"`
	// Priority is the priority of the page among the pages of its site, the content of
	// the <priority> of a <url>: between 0.0 and 1.0 by the protocol. It is nil if the
	// element is absent, and if what it holds is no number, which is reported. Tolerant
	// mode returns a value outside that range as it is. Strict mode skips and reports a
	// <url> whose <priority> is outside it or is no number.
	Priority *float32 `xml:"priority"`
	// Images are the images on the page, one for each <image:image> of the <url> that
	// has a location, in the order of the document. It is empty or nil if there is
	// none.
	Images []Image `xml:"http://www.google.com/schemas/sitemap-image/1.1 image"`
	// News is what the <url> tells about the page as a news article, its <news:news>.
	// It is nil if the <url> has none.
	News *News `xml:"http://www.google.com/schemas/sitemap-news/0.9 news"`
	// Videos are the videos on the page, one for each <video:video> of the <url> that
	// has a thumbnail location, in the order of the document. It is empty or nil if
	// there is none.
	Videos []Video `xml:"http://www.google.com/schemas/sitemap-video/1.1 video"`
	// Hreflangs are the versions of the page in other languages and for other regions,
	// one for each <xhtml:link> of the <url> that has an href, in the order of the
	// document. It is empty or nil if there is none.
	Hreflangs []AlternateLink `xml:"http://www.w3.org/1999/xhtml link"`
}

URL is a structure of <url> in <urlset>. It is also what a page of a sitemap in another format is returned as: an item of an RSS feed, an entry of an Atom feed, a line of a text sitemap.

LastMod is the date the page was last modified: the <lastmod> of a <url>, the <pubDate> of an RSS item and the <updated> of an Atom entry. A text sitemap gives no date. No other field but Loc is set for a page of a feed or of a text sitemap.

In the URLs that Parse and ParseContext collect, no text value has whitespace around it: the whitespace that surrounds the content of an element or the value of an attribute is not taken for a part of it. This goes for the fields of the extensions as well.

In tolerant mode Loc is a URL that can be requested as it is: a space or a character that is not in ASCII is percent-encoded in it, and an internationalised host name is in its Punycode form. A URL of an extension comes back the same way if it is an absolute HTTP or HTTPS URL, and as the document gives it otherwise: Image.Loc and Image.License, Video.ThumbnailLoc, Video.ContentLoc, Video.PlayerLoc and VideoUploader.Info, and AlternateLink.Href. Strict mode returns every URL as the document gives it.

Example

The fields of a URL other than its location are pointers, which are nil for what the entry does not give.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2024-03-01</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.8</priority>
  </url>
  <url>
    <loc>https://example.com/about</loc>
  </url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		fmt.Println(u.Loc)
		if u.LastMod != nil {
			fmt.Println("  last modified:", u.LastMod.Format(time.DateOnly))
		}
		if u.ChangeFreq != nil {
			fmt.Println("  changes:", *u.ChangeFreq)
		}
		if u.Priority != nil {
			fmt.Println("  priority:", *u.Priority)
		}
	}
}
Output:
https://example.com/
  last modified: 2024-03-01
  changes: daily
  priority: 0.8
https://example.com/about

type URLChangeFreq added in v0.3.0

type URLChangeFreq string

URLChangeFreq represents the frequency at which a URL should be crawled and indexed. Possible values are: "always", "hourly", "daily", "weekly", "monthly", "yearly", and "never".

In the URLs that Parse and ParseContext collect, a field of this type is nil when the <changefreq> element is absent or empty. In tolerant mode one of the values above that is written in another letter case, such as "Daily", is replaced by the value itself, so that it equals its constant, and any other value is kept as the document gives it. Strict mode skips and reports an entry whose value is not one of the values above as they are written here, so a field that is set equals one of the constants there.

Example

A change frequency is compared with the constants of its type. Tolerant mode reads a value of the protocol in whichever letter case it is written.

package main

import (
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/news</loc><changefreq>hourly</changefreq></url>
  <url><loc>https://example.com/blog</loc><changefreq>Daily</changefreq></url>
  <url><loc>https://example.com/about</loc><changefreq>yearly</changefreq></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		switch *u.ChangeFreq {
		case sitemap.ChangeFreqAlways, sitemap.ChangeFreqHourly, sitemap.ChangeFreqDaily:
			fmt.Println(u.Loc, "changes often")
		default:
			fmt.Println(u.Loc, "changes seldom")
		}
	}
}
Output:
https://example.com/news changes often
https://example.com/blog changes often
https://example.com/about changes seldom
const (
	// ChangeFreqAlways represents the "always" value for URLChangeFreq.
	ChangeFreqAlways URLChangeFreq = "always"

	// ChangeFreqHourly represents the "hourly" value for URLChangeFreq.
	ChangeFreqHourly URLChangeFreq = "hourly"

	// ChangeFreqDaily represents the "daily" value for URLChangeFreq.
	ChangeFreqDaily URLChangeFreq = "daily"

	// ChangeFreqWeekly represents the "weekly" value for URLChangeFreq.
	ChangeFreqWeekly URLChangeFreq = "weekly"

	// ChangeFreqMonthly represents the "monthly" value for URLChangeFreq.
	ChangeFreqMonthly URLChangeFreq = "monthly"

	// ChangeFreqYearly represents the "yearly" value for URLChangeFreq.
	ChangeFreqYearly URLChangeFreq = "yearly"

	// ChangeFreqNever represents the "never" value for URLChangeFreq.
	ChangeFreqNever URLChangeFreq = "never"
)

type ValidationError added in v0.9.0

type ValidationError struct {
	// URL is the URL value being validated. When it is not a URL that was rejected, it is the
	// URL the rejected value belongs to: the page for a value of a <url> entry and for the
	// date of an entry of a feed, the sitemap a sitemap index lists for the date the index
	// gives for it, and the sitemap an entry stands in for an entry without a location. For
	// a sitemap reached through a redirect the latter is the URL the sitemap was served from.
	// May be empty for field-level errors where no specific URL is available.
	URL string
	// Err is the underlying validation error.
	Err error
}

ValidationError is returned when a URL or field value fails validation. Callers can inspect URL to determine which value was rejected.

How many entries of a document are not valid is up to the document, so a call records no more errors of this type than the limit set with SetMaxValidationErrors allows.

Example usage:

var valErr *sitemap.ValidationError
if errors.As(err, &valErr) {
    fmt.Println("invalid URL:", valErr.URL)
}
Example

An entry that is not valid is reported by a ValidationError, which names the URL that was rejected, or the page a rejected value belongs to.

package main

import (
	"errors"
	"fmt"
	"log"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>ftp://example.com/files</loc></url>
  <url><loc>https://example.com/about</loc><priority>high</priority></url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, err := range s.GetErrors() {
		var validationErr *sitemap.ValidationError
		if errors.As(err, &validationErr) {
			fmt.Println(validationErr.URL, "-", validationErr.Err)
		}
	}
}
Output:
ftp://example.com/files - unsupported scheme "ftp"
https://example.com/about - invalid <priority> value "high"

func (*ValidationError) Error added in v0.9.0

func (e *ValidationError) Error() string

Error returns the message of the error, which names the URL and tells what is not valid: `validate "ftp://example.com/page": unsupported scheme "ftp"`.

func (*ValidationError) Unwrap added in v0.9.0

func (e *ValidationError) Unwrap() error

Unwrap returns the underlying error, enabling errors.Is / errors.As traversal.

type Video added in v0.8.0

type Video struct {
	// ThumbnailLoc is the URL of the thumbnail image of the video, the content of
	// <video:thumbnail_loc>. It is never empty, and it may be on another host than the
	// page. Strict mode requires an absolute HTTP or HTTPS URL: a video whose
	// ThumbnailLoc is not one is left out and reported.
	ThumbnailLoc string `xml:"http://www.google.com/schemas/sitemap-video/1.1 thumbnail_loc"`
	// Title is the title of the video, the content of <video:title>. Strict mode
	// reports a video without one.
	Title string `xml:"http://www.google.com/schemas/sitemap-video/1.1 title"`
	// Description is the description of the video, the content of
	// <video:description>. Strict mode reports a video without one.
	Description string `xml:"http://www.google.com/schemas/sitemap-video/1.1 description"`
	// ContentLoc is the URL of the video file itself, the content of
	// <video:content_loc>. Strict mode reports a video that has neither a ContentLoc
	// nor a PlayerLoc, and a ContentLoc that is no absolute HTTP or HTTPS URL.
	ContentLoc string `xml:"http://www.google.com/schemas/sitemap-video/1.1 content_loc"`
	// PlayerLoc is the URL of a player for the video, the content of
	// <video:player_loc>. Strict mode reports one that is no absolute HTTP or HTTPS
	// URL, see ContentLoc.
	PlayerLoc string `xml:"http://www.google.com/schemas/sitemap-video/1.1 player_loc"`
	// Duration is the length of the video in seconds, the content of <video:duration>.
	// Strict mode reports a value that is not between 1 and 28800, which is eight
	// hours.
	Duration *int `xml:"http://www.google.com/schemas/sitemap-video/1.1 duration"`
	// ExpirationDate is the date after which the video is no longer available, the
	// content of <video:expiration_date>. It is nil for an element that is empty as
	// well.
	ExpirationDate *LastModTime `xml:"http://www.google.com/schemas/sitemap-video/1.1 expiration_date"`
	// Rating is the rating of the video, the content of <video:rating>. Strict mode
	// reports a value that is not between 0.0 and 5.0.
	Rating *float32 `xml:"http://www.google.com/schemas/sitemap-video/1.1 rating"`
	// ViewCount is the number of times the video was viewed, the content of
	// <video:view_count>.
	ViewCount *int `xml:"http://www.google.com/schemas/sitemap-video/1.1 view_count"`
	// PublicationDate is the date the video was first published, the content of
	// <video:publication_date>. It is nil for an element that is empty as well.
	PublicationDate *LastModTime `xml:"http://www.google.com/schemas/sitemap-video/1.1 publication_date"`
	// FamilyFriendly tells whether the video is fit to be shown with a filter for
	// adult content on, the content of <video:family_friendly>: "yes" or "no" by the
	// extension. Neither mode checks the value.
	FamilyFriendly string `xml:"http://www.google.com/schemas/sitemap-video/1.1 family_friendly"`
	// Restriction tells the countries the video may or may not be shown in, the
	// <video:restriction> of the video.
	Restriction *VideoRestriction `xml:"http://www.google.com/schemas/sitemap-video/1.1 restriction"`
	// Platform tells the platforms the video may or may not be shown on, the
	// <video:platform> of the video.
	Platform *VideoPlatform `xml:"http://www.google.com/schemas/sitemap-video/1.1 platform"`
	// RequiresSubscription tells whether the video can only be viewed with a
	// subscription, the content of <video:requires_subscription>: "yes" or "no" by the
	// extension. Neither mode checks the value.
	RequiresSubscription string `xml:"http://www.google.com/schemas/sitemap-video/1.1 requires_subscription"`
	// Uploader is who uploaded the video, the <video:uploader> of the video.
	Uploader *VideoUploader `xml:"http://www.google.com/schemas/sitemap-video/1.1 uploader"`
	// Live tells whether the video is a live stream, the content of <video:live>:
	// "yes" or "no" by the extension. Neither mode checks the value.
	Live string `xml:"http://www.google.com/schemas/sitemap-video/1.1 live"`
	// Tags are the tags of the video, one for each <video:tag> and in the order of
	// the document. It is nil if the video has none. Strict mode reports a video with
	// more than 32 of them.
	Tags []string `xml:"http://www.google.com/schemas/sitemap-video/1.1 tag"`
}

Video is a structure of <video:video> in <url>, per the Google Video Sitemap extension. Reference: https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps

A video is known by its ThumbnailLoc, and one without a ThumbnailLoc is not among the videos of a URL. A text field is empty, and a pointer is nil, when the video has no element for it. A number or a date that cannot be parsed is nil as well, and is reported in both modes; the video is returned without it.

Strict mode reports what the extension requires and a video lacks or has out of range, as the fields below tell. A video whose ThumbnailLoc is accepted is returned whatever else is reported about it.

Example

The numbers and the dates of a video are pointers, which are nil for what the video does not give.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aafeher/go-sitemap-parser"
)

func main() {
	content := `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
  <url>
    <loc>https://example.com/videos/harbour</loc>
    <video:video>
      <video:thumbnail_loc>https://cdn.example.net/harbour.jpg</video:thumbnail_loc>
      <video:title>A walk around the harbour</video:title>
      <video:description>The harbour at dawn, from the pier to the lighthouse.</video:description>
      <video:content_loc>https://cdn.example.net/harbour.mp4</video:content_loc>
      <video:duration>600</video:duration>
      <video:restriction relationship="allow">IE GB</video:restriction>
      <video:tag>harbour</video:tag>
      <video:tag>walk</video:tag>
    </video:video>
  </url>
</urlset>`

	s := sitemap.New()
	if _, err := s.Parse("https://example.com/sitemap.xml", &content); err != nil {
		log.Fatal(err)
	}

	for _, u := range s.GetURLs() {
		for _, video := range u.Videos {
			fmt.Println(video.Title)
			fmt.Println(video.ContentLoc)
			if video.Duration != nil {
				fmt.Println(time.Duration(*video.Duration) * time.Second)
			}
			if video.Restriction != nil {
				fmt.Println(video.Restriction.Relationship, video.Restriction.Value)
			}
			if video.Rating == nil {
				fmt.Println("no rating")
			}
			fmt.Println(video.Tags)
		}
	}
}
Output:
A walk around the harbour
https://cdn.example.net/harbour.mp4
10m0s
allow IE GB
no rating
[harbour walk]

type VideoPlatform added in v0.8.0

type VideoPlatform struct {
	// Relationship tells whether the video may be shown on the platforms Value names or
	// may not be shown there, the value of the relationship attribute: "allow" or
	// "deny" by the extension. Neither mode checks the value.
	Relationship string `xml:"relationship,attr"`
	// Value names the platforms, as the content of the element gives them: "web",
	// "mobile" and "tv" with a space between them by the extension, such as "web tv".
	Value string `xml:",chardata"`
}

VideoPlatform is a structure of <video:platform> in <video:video>. It captures the element text and the required "relationship" attribute.

type VideoRestriction added in v0.8.0

type VideoRestriction struct {
	// Relationship tells whether the video may be shown in the countries Value names or
	// may not be shown there, the value of the relationship attribute: "allow" or
	// "deny" by the extension. Neither mode checks the value.
	Relationship string `xml:"relationship,attr"`
	// Value names the countries, as the content of the element gives them: country
	// codes with a space between them by the extension, such as "IE GB US".
	Value string `xml:",chardata"`
}

VideoRestriction is a structure of <video:restriction> in <video:video>. It captures the element text and the required "relationship" attribute.

type VideoUploader added in v0.8.0

type VideoUploader struct {
	// Info is the URL of a page that tells more about the uploader, the value of the
	// info attribute. It is empty if the element has no such attribute. Strict mode
	// reports one that is no absolute HTTP or HTTPS URL; the video is returned all the
	// same, with the value as the document gives it.
	Info string `xml:"info,attr"`
	// Value is the name of the uploader, the content of the element.
	Value string `xml:",chardata"`
}

VideoUploader is a structure of <video:uploader> in <video:video>. It captures the uploader name and the optional "info" URL attribute.

Directories

Path Synopsis
examples
advanced command
atom command
context command
encoding command
errors command
follow command
getrandomurls command
geturls command
gzip command
hreflang command
httpclient command
idn command
image command
iterators command
lastmod command
maxconcurrency command
maxdepth command
maxresponsesize command
maxsitemaps command
maxurls command
multithread command
news command
redirect command
reuse command
robots command
rss command
rules command
simple command
sitemaps command
strict command
text command
tolerant command
video command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL