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
This commit is contained in:
105
classes/serialize.go
Normal file
105
classes/serialize.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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("&", "&", "<", "<", ">", ">", "\r", " ")
|
||||
|
||||
// attrEscaper is libxml's escaping of attribute values: also the double
|
||||
// quote, tab and newline; the single quote stays literal.
|
||||
var attrEscaper = strings.NewReplacer(
|
||||
"&", "&", "<", "<", ">", ">", `"`, """,
|
||||
"\r", " ", "\t", "	", "\n", " ",
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user