GamendWeb.Sitemap.Xml (gamend_web v1.0.1216)

Copy Markdown View Source

Rendering a <urlset> or a <sitemapindex>: the parts no host varies.

GamendWeb.Sitemap.Source says which pages exist and GamendWeb.Sitemap.Lastmod says when they changed; this turns the answers into bytes. Which pages, and where their dates come from, stay the host's — this owns the XML, the hreflang alternates and the locale URL shape, which are the sitemaps protocol rather than anybody's content.

Both hosts had a byte-identical copy of every function here. They were not even a fork: the second was pasted from the first and then both were edited.

Iodata throughout

A large sitemap is ~5,000 URLs and ~19 MB, 90% of it the alternate set every URL carries. Nothing is joined into a binary on the way out — alternate_links/2 is built once per page and shared by that page's 30 locale entries as a pointer rather than a 3 KB copy each time, and Plug.Conn.send_resp/3 takes the nested list directly.

What is not here

No <changefreq> and no <priority>: Google ignores both outright and Bing effectively does, so they were bytes advertising nothing.

Summary

Types

A page to render. :loc is a path, not a URL — the host names the route and this prefixes the endpoint.

Functions

One xhtml:link alternate.

The xhtml:link alternate set for one path, as shared iodata.

The endpoint's URL with any trailing slash removed, so <> path is well-formed.

Everything a URL entry needs that is the same for every URL in the file.

One <url> entry per page, or one per locale when the page is translated.

A <lastmod> line, or nothing at all when the date is unknown.

The URL a path has in one locale.

Every locale a translated page is advertised in, default first.

A page's own date: a literal one, the manifest's, or none.

A <sitemapindex> over {url, lastmod} children.

Wraps rendered <url> entries in a <urlset>.

Types

page()

@type page() :: %{
  :loc => String.t(),
  optional(:lastmod) => String.t() | nil,
  optional(:lastmod_key) => String.t()
}

A page to render. :loc is a path, not a URL — the host names the route and this prefixes the endpoint.

Its date comes from :lastmod when the page carries a literal one (a blog post's front matter), from :lastmod_key when the manifest tracks it, and is absent otherwise. Absent is a real answer, not a gap to fill: a date derived from a file mtime is a lie that costs the whole sitemap its credibility.

Functions

alternate(hreflang, href)

@spec alternate(String.t(), String.t()) :: iodata()

One xhtml:link alternate.

alternate_links(ctx, loc)

@spec alternate_links(map(), String.t()) :: iodata()

The xhtml:link alternate set for one path, as shared iodata.

Build it once per page, not once per entry: this is the 90%. x-default points at the clean URL, which is what tells a crawler which one to serve when it has no better signal.

base_url()

@spec base_url() :: String.t()

The endpoint's URL with any trailing slash removed, so <> path is well-formed.

context(opts \\ [])

@spec context(keyword()) :: map()

Everything a URL entry needs that is the same for every URL in the file.

Read once per sitemap rather than per entry: Source.manifest/0 stats the file on every call and the locale list is rebuilt each time — times 5,000 URLs × 30 locales, that was the bulk of rendering a large one.

Options:

  • :manifest — a manifest already loaded, when the caller needs it too
  • :locale_lastmod(own_lastmod, locale -> lastmod), for a host whose translated pages change on their own schedule. The default returns the page's own date for every locale.
  • :extra — merged into the returned map, for whatever else the host's entry building needs.

entries_for(ctx, page)

@spec entries_for(map(), page()) :: [iodata()]

One <url> entry per page, or one per locale when the page is translated.

Whether a path is translated is LocalePath.localized_path?/1 — the same answer the router gives, so a page cannot be advertised under a prefix that does not serve it.

lastmod_tag(lastmod)

@spec lastmod_tag(String.t() | nil) :: iodata()

A <lastmod> line, or nothing at all when the date is unknown.

locale_url(ctx, locale, loc)

@spec locale_url(map(), String.t(), String.t()) :: String.t()

The URL a path has in one locale.

The default locale lives at the clean URL and every other is prefixed — the root is the one path that takes no suffix, or it would render /fr/.

locales()

@spec locales() :: [String.t()]

Every locale a translated page is advertised in, default first.

Order matters: the default locale owns the clean URL, so it leads and the rest follow prefixed. A host that needs the list before it can build a context — to precompute something per locale and close over it — calls this rather than deriving it again.

own_lastmod(ctx, arg2)

@spec own_lastmod(map(), page()) :: String.t() | nil

A page's own date: a literal one, the manifest's, or none.

sitemapindex(children)

@spec sitemapindex([{String.t(), String.t() | nil}]) :: iodata()

A <sitemapindex> over {url, lastmod} children.

Takes whole URLs rather than paths, because an index may point at another host entirely — which the protocol allows and entries_for/2 does not.

url_entry(loc, lastmod, alternates)

@spec url_entry(String.t(), String.t() | nil, iodata()) :: iodata()

One <url> element.

urlset(urls)

@spec urlset([iodata()]) :: iodata()

Wraps rendered <url> entries in a <urlset>.