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 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 , with an absolute // href. type FileAlternate struct { Hreflang string Href string } // FileEntry is one 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--.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 per file with its // and, when known, its . func WriteIndex(w io.Writer, files []IndexFile) error { bw := bufio.NewWriter(w) bw.WriteString(xml.Header) bw.WriteString(``) for _, f := range files { bw.WriteString("\n") escape(bw, f.Loc) bw.WriteString("") if f.LastMod != nil { bw.WriteString("" + lastmod(*f.LastMod) + "") } bw.WriteString("") } bw.WriteString("\n\n") return bw.Flush() } // WriteURLSet writes one sitemap file: a per entry with its , its // when known and its xhtml:link alternates. The xhtml namespace // is declared only when an entry has alternates. No and no // are written (D-30). func WriteURLSet(w io.Writer, entries []FileEntry) error { bw := bufio.NewWriter(w) bw.WriteString(xml.Header) bw.WriteString(` 0 { bw.WriteString(` xmlns:xhtml="` + NSXHTML + `"`) break } } bw.WriteString(">") for _, e := range entries { bw.WriteString("\n") escape(bw, e.Loc) bw.WriteString("") if e.LastMod != nil { bw.WriteString("" + lastmod(*e.LastMod) + "") } for _, a := range e.Alternates { bw.WriteString(``) } bw.WriteString("") } bw.WriteString("\n\n") return bw.Flush() } // ImageEntry is one 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 per page with its // and one 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(``) for _, e := range entries { bw.WriteString("\n") escape(bw, e.Loc) bw.WriteString("") for i, img := range e.Images { if i >= MaxImagesPerURL { break } bw.WriteString("") escape(bw, img) bw.WriteString("") } bw.WriteString("") } bw.WriteString("\n\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) }