Files
sm-sitemap-plugin/classes/serialize.go
Jakub Zych fe39e26ce0 feat(17-03): sitemap plugin tracer, /sitemap.xml byte-identical to PHP
- golem15.sitemap plugin: golem15_sitemap_definitions migration, Definition
  model (Fillable theme and data) with tolerant PHP-shaped item decoding and
  PHP toArray item encoding
- Url::to and buildUrl port, libxml-format serializer, generator for url and
  registry items
- GET /sitemap.xml serves the definition with theme default (404 without one,
  empty body without items)
- tests/php/gen-goldens.php and the tracer golden; TestSitemapGolden and the
  Postgres TestSitemapTracer
2026-10-12 16:53:09 +02:00

106 lines
3.6 KiB
Go

package classes
import "strings"
// The sitemap document is written by hand in the exact format PHP's
// DOMDocument::saveXML (libxml2) produced for the PHP plugin: the XML
// declaration and the stylesheet processing instruction each followed by a
// newline, everything else on one line, an empty urlset self-closed,
// xhtml:link elements self-closed without a space. The standard library's
// XML encoder escapes and closes elements differently, so the writer is a
// strings.Builder.
// The urlset opening tag's attributes, in the order PHP set them.
const urlsetAttrs = ` xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"` +
` xmlns:xhtml="http://www.w3.org/1999/xhtml"` +
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"` +
` xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"`
// textEscaper is libxml's escaping of text nodes: &, <, > and carriage
// return; quotes, tabs, newlines and UTF-8 stay literal.
var textEscaper = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", "\r", "&#13;")
// attrEscaper is libxml's escaping of attribute values: also the double
// quote, tab and newline; the single quote stays literal.
var attrEscaper = strings.NewReplacer(
"&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;",
"\r", "&#13;", "\t", "&#9;", "\n", "&#10;",
)
// EscapeText escapes s as libxml writes a text node.
func EscapeText(s string) string { return textEscaper.Replace(s) }
// EscapeAttr escapes s as libxml writes an attribute value.
func EscapeAttr(s string) string { return attrEscaper.Replace(s) }
// Alternate is one hreflang link of a sitemap URL.
type Alternate struct {
Hreflang string
Href string
}
// URLEntry is one url element: Loc, an optional Lastmod, Changefreq and
// Priority as they are written, the alternates (written only when there are
// more than one) and Extra, pre-escaped child XML that AddItem listeners
// appended, written before the closing tag.
type URLEntry struct {
Loc string
Lastmod string
Changefreq string
Priority string
Alternates []Alternate
Extra []string
}
// writeURL appends one url element.
func writeURL(b *strings.Builder, e URLEntry) {
b.WriteString("<url><loc>")
b.WriteString(EscapeText(e.Loc))
b.WriteString("</loc>")
if e.Lastmod != "" {
b.WriteString("<lastmod>")
b.WriteString(EscapeText(e.Lastmod))
b.WriteString("</lastmod>")
}
b.WriteString("<changefreq>")
b.WriteString(EscapeText(e.Changefreq))
b.WriteString("</changefreq><priority>")
b.WriteString(EscapeText(e.Priority))
b.WriteString("</priority>")
if len(e.Alternates) > 1 {
for _, a := range e.Alternates {
b.WriteString(`<xhtml:link rel="alternate" hreflang="`)
b.WriteString(EscapeAttr(a.Hreflang))
b.WriteString(`" href="`)
b.WriteString(EscapeAttr(a.Href))
b.WriteString(`"/>`)
}
}
for _, x := range e.Extra {
b.WriteString(x)
}
b.WriteString("</url>")
}
// Serialize writes the sitemap document: the XML declaration, the
// stylesheet processing instruction pointing at stylesheet (written as it
// is, like libxml writes PI data) and the urlset with entries.
func Serialize(stylesheet string, entries []URLEntry) string {
var b strings.Builder
b.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
b.WriteString(`<?xml-stylesheet type="text/xsl" href="`)
b.WriteString(stylesheet)
b.WriteString("\"?>\n<urlset")
b.WriteString(urlsetAttrs)
if len(entries) == 0 {
b.WriteString("/>\n")
return b.String()
}
b.WriteString(">")
for _, e := range entries {
writeURL(&b, e)
}
b.WriteString("</urlset>\n")
return b.String()
}