- migration 202610280004: image_file_no, kind, published_at and parent_refs on the slot table; the image, News, IndexNow and HTML sitemap settings - the rebuild stores each page's featured and body images (absolute on the site, deduplicated, at most 1000) and numbers the rows with images into sitemap-images-<n>.xml with the per-type files' stable rules - the index lists the image files while images_enabled is on; the files route serves them with image:image and image:loc only
169 lines
4.8 KiB
Go
169 lines
4.8 KiB
Go
package classes
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/xml"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The XML namespaces of the sitemap files.
|
|
const (
|
|
NSSitemap = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
NSXHTML = "http://www.w3.org/1999/xhtml"
|
|
NSImage = "http://www.google.com/schemas/sitemap-image/1.1"
|
|
)
|
|
|
|
// IndexFile is one <sitemap> of the index: the file's absolute URL and the
|
|
// newest lastmod of its addresses (nil when none has one).
|
|
type IndexFile struct {
|
|
Loc string
|
|
LastMod *time.Time
|
|
}
|
|
|
|
// FileAlternate is one xhtml:link alternate of a <url>, with an absolute
|
|
// href.
|
|
type FileAlternate struct {
|
|
Hreflang string
|
|
Href string
|
|
}
|
|
|
|
// FileEntry is one <url> of a file: the absolute address, its lastmod and
|
|
// its hreflang alternates.
|
|
type FileEntry struct {
|
|
Loc string
|
|
LastMod *time.Time
|
|
Alternates []FileAlternate
|
|
}
|
|
|
|
// fileNamePattern is the name of a per-group file.
|
|
var fileNamePattern = regexp.MustCompile(`^sitemap-([a-z0-9][a-z0-9-]*)-([1-9][0-9]{0,8})\.xml$`)
|
|
|
|
// FileName is the name of file n of group grp: sitemap-<grp>-<n>.xml.
|
|
func FileName(grp string, n int) string {
|
|
return "sitemap-" + grp + "-" + strconv.Itoa(n) + ".xml"
|
|
}
|
|
|
|
// ParseFileName splits a file name made by FileName; ok is false for any
|
|
// other name.
|
|
func ParseFileName(name string) (grp string, n int, ok bool) {
|
|
m := fileNamePattern.FindStringSubmatch(name)
|
|
if m == nil {
|
|
return "", 0, false
|
|
}
|
|
n, err := strconv.Atoi(m[2])
|
|
if err != nil || n < 1 {
|
|
return "", 0, false
|
|
}
|
|
return m[1], n, true
|
|
}
|
|
|
|
// lastmod formats a lastmod in RFC 3339, UTC.
|
|
func lastmod(t time.Time) string { return t.UTC().Format(time.RFC3339) }
|
|
|
|
// escape writes s with encoding/xml's escaping, which covers &, <, >, both
|
|
// quotes and control whitespace, so it is safe in text and in attribute
|
|
// values.
|
|
func escape(w *bufio.Writer, s string) {
|
|
_ = xml.EscapeText(w, []byte(s))
|
|
}
|
|
|
|
// WriteIndex writes the sitemap index: one <sitemap> per file with its
|
|
// <loc> and, when known, its <lastmod>.
|
|
func WriteIndex(w io.Writer, files []IndexFile) error {
|
|
bw := bufio.NewWriter(w)
|
|
bw.WriteString(xml.Header)
|
|
bw.WriteString(`<sitemapindex xmlns="` + NSSitemap + `">`)
|
|
for _, f := range files {
|
|
bw.WriteString("\n<sitemap><loc>")
|
|
escape(bw, f.Loc)
|
|
bw.WriteString("</loc>")
|
|
if f.LastMod != nil {
|
|
bw.WriteString("<lastmod>" + lastmod(*f.LastMod) + "</lastmod>")
|
|
}
|
|
bw.WriteString("</sitemap>")
|
|
}
|
|
bw.WriteString("\n</sitemapindex>\n")
|
|
return bw.Flush()
|
|
}
|
|
|
|
// WriteURLSet writes one sitemap file: a <url> per entry with its <loc>, its
|
|
// <lastmod> when known and its xhtml:link alternates. The xhtml namespace
|
|
// is declared only when an entry has alternates. No <priority> and no
|
|
// <changefreq> are written (D-30).
|
|
func WriteURLSet(w io.Writer, entries []FileEntry) error {
|
|
bw := bufio.NewWriter(w)
|
|
bw.WriteString(xml.Header)
|
|
bw.WriteString(`<urlset xmlns="` + NSSitemap + `"`)
|
|
for _, e := range entries {
|
|
if len(e.Alternates) > 0 {
|
|
bw.WriteString(` xmlns:xhtml="` + NSXHTML + `"`)
|
|
break
|
|
}
|
|
}
|
|
bw.WriteString(">")
|
|
for _, e := range entries {
|
|
bw.WriteString("\n<url><loc>")
|
|
escape(bw, e.Loc)
|
|
bw.WriteString("</loc>")
|
|
if e.LastMod != nil {
|
|
bw.WriteString("<lastmod>" + lastmod(*e.LastMod) + "</lastmod>")
|
|
}
|
|
for _, a := range e.Alternates {
|
|
bw.WriteString(`<xhtml:link rel="alternate" hreflang="`)
|
|
escape(bw, a.Hreflang)
|
|
bw.WriteString(`" href="`)
|
|
escape(bw, a.Href)
|
|
bw.WriteString(`"/>`)
|
|
}
|
|
bw.WriteString("</url>")
|
|
}
|
|
bw.WriteString("\n</urlset>\n")
|
|
return bw.Flush()
|
|
}
|
|
|
|
// ImageEntry is one <url> of an image sitemap: the page's absolute address
|
|
// and its images' absolute addresses (at most MaxImagesPerURL).
|
|
type ImageEntry struct {
|
|
Loc string
|
|
Images []string
|
|
}
|
|
|
|
// WriteImageURLSet writes one image sitemap file: a <url> per page with its
|
|
// <loc> and one <image:image><image:loc> per image. Only image:image and
|
|
// image:loc are written, the tags Google still reads (D-31).
|
|
func WriteImageURLSet(w io.Writer, entries []ImageEntry) error {
|
|
bw := bufio.NewWriter(w)
|
|
bw.WriteString(xml.Header)
|
|
bw.WriteString(`<urlset xmlns="` + NSSitemap + `" xmlns:image="` + NSImage + `">`)
|
|
for _, e := range entries {
|
|
bw.WriteString("\n<url><loc>")
|
|
escape(bw, e.Loc)
|
|
bw.WriteString("</loc>")
|
|
for i, img := range e.Images {
|
|
if i >= MaxImagesPerURL {
|
|
break
|
|
}
|
|
bw.WriteString("<image:image><image:loc>")
|
|
escape(bw, img)
|
|
bw.WriteString("</image:loc></image:image>")
|
|
}
|
|
bw.WriteString("</url>")
|
|
}
|
|
bw.WriteString("\n</urlset>\n")
|
|
return bw.Flush()
|
|
}
|
|
|
|
// validSlug reports whether s may name a file group.
|
|
func validSlug(s string) bool {
|
|
return s != "" && len(s) <= 64 && fileNamePattern.MatchString(FileName(s, 1)) && !strings.HasSuffix(s, "-")
|
|
}
|
|
|
|
// ValidSlug reports whether s may name a file group: lowercase letters,
|
|
// digits and hyphens, starting with a letter or digit, at most 64
|
|
// characters.
|
|
func ValidSlug(s string) bool { return validSlug(s) }
|