GamendWeb.Sitemap.Source behaviour (gamend_web v1.0.1215)

Copy Markdown View Source

The host's answer to "which pages exist, and what is on them".

Core owns the dating machinery (GamendWeb.Sitemap.Lastmod) but has no idea what a host's pages are made of — one host's page is a blog post, another's is a slice of a dictionary. The host implements this behaviour; the mix task and the change notifier both drive it through here.

Configure the implementation and where its manifest lives:

config :gamend_web, GamendWeb.Sitemap,
  source: GamendHost.Sitemap.Source,
  manifest: "priv/sitemap_lastmod.json"

Implementing it

entries/0 returns one entry per page whose date should be tracked. The :content is what a visitor reads, parsed — the words, the labels, the values — not the bytes of whatever file it was loaded from. That distinction is the whole point: it is what makes a re-export or a line-ending change invisible to the date.

Do not include a locale in the key. A page and its translations share one date, because they share one source of truth; emitting 30 keys per page would multiply the manifest by 30 to store the same date 30 times.

Summary

Callbacks

Every page to track, keyed by a stable identifier.

Everything besides the manifest that changes the rendered sitemap.

Every public URL a key is visible at — all of its locale variants.

Functions

The manifest, cached in :persistent_term and re-read when the file changes.

Path to the manifest.

The source's signature_inputs/0, or an empty list when it has none.

The configured source module, or nil when the host tracks no dates.

URLs for the keys that changed, via the source's urls/1.

Callbacks

entries()

@callback entries() :: [GamendWeb.Sitemap.Lastmod.entry()]

Every page to track, keyed by a stable identifier.

The key is the manifest key and outlives URLs, so prefer something derived from the content's identity ("vocabulary/polish/food") over the current path. Renaming a route should not reset every date.

signature_inputs()

(optional)
@callback signature_inputs() :: [term()]

Everything besides the manifest that changes the rendered sitemap.

Read by GamendWeb.Sitemap.Cache to build the signature its keys and ETags are scoped by. Core covers what every host shares — the manifest's stamp, the endpoint URL, the locale set — so this is for the host's own inputs: how many pages of each kind exist, the newest date of anything the manifest does not track, and a version number bumped by hand, since pages decided by code rather than data change the output and nothing derived from content notices.

Cheap: it runs on every sitemap request. Prefer a count or an mtime to a hash of the thing being counted.

Terms only — the list is hashed through :erlang.term_to_binary/1, so anything comparable works and nothing needs to be a string.

urls(key)

(optional)
@callback urls(key :: String.t()) :: [String.t()]

Every public URL a key is visible at — all of its locale variants.

Only needed to notify search engines of changes; mix gamend.sitemap.lastmod --notify maps the keys that moved through this and submits the result to GamendWeb.IndexNow. Routing belongs to the host, so the mapping does too.

Functions

manifest()

@spec manifest() :: GamendWeb.Sitemap.Lastmod.manifest()

The manifest, cached in :persistent_term and re-read when the file changes.

Sitemap requests are rare and the manifest is large — 9,000 pages is well over a megabyte of JSON — so decoding it per request would be the most expensive thing a sitemap does. Caching on mtime keeps that to one decode, while still picking up a restamped manifest without a restart, which matters when the file is bind-mounted rather than baked into the image.

manifest_path()

@spec manifest_path() :: Path.t()

Path to the manifest.

The manifest belongs to the host, not to core, so a relative path resolves against the working directory — the project root under Mix and the release root otherwise — rather than against any application's priv/. Hosts whose runtime working directory is not the checkout should configure an absolute path.

signature_inputs()

@spec signature_inputs() :: [term()]

The source's signature_inputs/0, or an empty list when it has none.

Empty is safe rather than silently wrong: the signature still covers the manifest and the endpoint, so a host without host-specific inputs simply caches on those.

source()

@spec source() :: module() | nil

The configured source module, or nil when the host tracks no dates.

urls_for(keys)

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

URLs for the keys that changed, via the source's urls/1.

An empty list when the source does not implement it — there is nothing sensible to guess, and submitting wrong URLs is worse than submitting none.