serve Atom feeds for the site, a section and a tag
Membership is a publication date, not a type declaration (ADR-0043). The parked
feed shape said "every type declared primary", which would have made feeds wait on
declared types a third time — but the thing that distinguishes a feed item is
already on disk. Pages and section landings drop out because they have no date,
which is the right reason. The parked idea stays parked with a sharper trigger:
someone wanting a *dated* bundle kept out.
Built with encoding/xml from typed structs, never a template: XML in html/template
is escaping for the wrong grammar, and that is a correctness trap rather than a
matter of taste.
A bug the evidence found, older than feeds: content.URL("", lang) built "//", so a
whole-site feed's id and alternate link were https://khosra.example// — every
entry identity wrong in every reader. The root is "/" now, with a test, and the
hand-built "/" the resolver carried for the same reason can follow later.
Two counters re-scoped rather than incremented, the same way transforms was:
Views now counts *per-bundle selection* — the thing architecture.md means by the
View layer, still at zero consumers. Output formats are not it: HTML, sitemap XML
and Atom are three functions with nothing to share, so an interface over them
would have one member and no leverage.
Effects stays at 1. A feed is generated per request like the sitemap, so it is not
a second Effect and the runner is not yet due — the next thing that writes files
off the request path is.
This commit is contained in:
@@ -20,6 +20,8 @@ type resolution struct {
|
||||
// redirect is the canonical path when the request named a non-canonical one. Non-empty means answer
|
||||
// with a permanent redirect and nothing else.
|
||||
redirect string
|
||||
// feed means the request named a feed of whatever scope key and tag describe (ADR-0043).
|
||||
feed bool
|
||||
}
|
||||
|
||||
// resolve maps a request path to a bundle key and language.
|
||||
@@ -54,6 +56,15 @@ func resolve(path string, site *content.Site) (resolution, bool) {
|
||||
return resolution{redirect: "/"}, true
|
||||
}
|
||||
|
||||
// A trailing feed.xml names a feed of the scope before it. It is a file rather than a page, so none of
|
||||
// the trailing-slash canonicalisation below applies to it (ADR-0043).
|
||||
if rest, isFeed := cutFeed(key); isFeed {
|
||||
if tag, section, isTag := cutTag(rest); isTag {
|
||||
return resolution{key: section, tag: tag, lang: lang, feed: true}, true
|
||||
}
|
||||
return resolution{key: rest, lang: lang, feed: true}, true
|
||||
}
|
||||
|
||||
page := 1
|
||||
// A trailing /page/N/ is pagination, not part of the key (ADR-0028). Page one is the bare listing
|
||||
// URL, so /page/1/ is a second spelling and redirects.
|
||||
@@ -80,6 +91,18 @@ func resolve(path string, site *content.Site) (resolution, bool) {
|
||||
return resolution{key: key, lang: lang, page: page}, true
|
||||
}
|
||||
|
||||
// cutFeed strips a trailing feed.xml, reporting whether one was there. What remains is the scope: empty for
|
||||
// the whole site, a section, or a tag path.
|
||||
func cutFeed(key string) (rest string, ok bool) {
|
||||
if key == feedFile {
|
||||
return "", true
|
||||
}
|
||||
if trimmed, found := strings.CutSuffix(key, "/"+feedFile); found {
|
||||
return trimmed, true
|
||||
}
|
||||
return key, false
|
||||
}
|
||||
|
||||
// cutTag splits a tag listing key into its term and the section it is narrowed to.
|
||||
func cutTag(key string) (tag, section string, ok bool) {
|
||||
if rest, found := strings.CutPrefix(key, content.TagsSegment+"/"); found {
|
||||
|
||||
Reference in New Issue
Block a user