feat: HTML sitemap groups API
- GET /_sitemap/api/v1/html?locale= answers the overview: the listed
addresses of the XML sitemaps by group, in the order pages, offers,
services, categories (as a tree), posts (newest first), tags, authors,
other content types, each with its title in the locale, total, the
first html_page_size links and more
- GET /_sitemap/api/v1/html/{group}?locale=&page= answers one page of a
group; past the last page, an unknown group or the HTML sitemap off
answer 404; every answer is noindex; per-IP bucket sitemap-html
- the HTML sitemap tab: html_enabled, the address per locale (html_paths)
and html_page_size 50 to 500, with errors.html_path and errors.page_size
- contract.Listing.Settings answers HTMLEnabled, HTMLPaths, HTMLPageSize
and KeyFile
This commit is contained in:
116
controllers/api/html.go
Normal file
116
controllers/api/html.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GroupPattern is the {group} constraint of the HTML sitemap group route:
|
||||
// a file group slug.
|
||||
const GroupPattern = `[a-z0-9][a-z0-9-]{0,63}`
|
||||
|
||||
// localePattern is the form a locale query value must have.
|
||||
var localePattern = regexp.MustCompile(`^[a-z]{2,3}([-_][A-Za-z0-9]{2,8})*$`)
|
||||
|
||||
// pagePattern is the form of the page query value: 1 to 6 digits.
|
||||
var pagePattern = regexp.MustCompile(`^[0-9]{1,6}$`)
|
||||
|
||||
// htmlRequest checks the shared parts of an HTML sitemap request and
|
||||
// returns the settings and database, or answers 404 (the HTML sitemap is
|
||||
// off, a malformed locale) or 500 itself.
|
||||
func htmlRequest(app func() *backpack.App, w http.ResponseWriter, r *http.Request) (classes.HTMLSitemap, bool) {
|
||||
w.Header().Set("X-Robots-Tag", "noindex")
|
||||
w.Header().Set("Cache-Control", "no-cache, private")
|
||||
a := app()
|
||||
if a == nil {
|
||||
http.NotFound(w, r)
|
||||
return classes.HTMLSitemap{}, false
|
||||
}
|
||||
if loc := r.URL.Query().Get("locale"); loc != "" && !localePattern.MatchString(loc) {
|
||||
http.NotFound(w, r)
|
||||
return classes.HTMLSitemap{}, false
|
||||
}
|
||||
db, ok := a.Lookup[*gorm.DB]()
|
||||
if !ok || db == nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return classes.HTMLSitemap{}, false
|
||||
}
|
||||
return classes.HTMLSitemap{App: a, DB: db.WithContext(r.Context())}, true
|
||||
}
|
||||
|
||||
// HTMLOverview serves GET /_sitemap/api/v1/html?locale=: the HTML sitemap
|
||||
// overview (D-34; classes.HTMLSitemap.Overview). It lists exactly the
|
||||
// addresses of the XML sitemaps and answers 404 while the HTML sitemap is
|
||||
// off. Every answer carries X-Robots-Tag: noindex.
|
||||
func HTMLOverview(app func() *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h, ok := htmlRequest(app, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
settings, err := classes.LoadSettings(h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !settings.HTMLEnabled {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
out, err := h.Overview(r.Context(), settings, r.URL.Query().Get("locale"))
|
||||
if err != nil {
|
||||
log.Error("golem15.sitemap: html overview", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
wire.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLGroup serves GET /_sitemap/api/v1/html/{group}?locale=&page=: one
|
||||
// page (from 1, the default) of a group (classes.HTMLSitemap.Group). An
|
||||
// unknown group, a page past the last, a malformed page or the HTML
|
||||
// sitemap off answer 404.
|
||||
func HTMLGroup(app func() *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h, ok := htmlRequest(app, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page := 1
|
||||
if raw := r.URL.Query().Get("page"); raw != "" {
|
||||
n, err := strconv.Atoi(raw)
|
||||
if !pagePattern.MatchString(raw) || err != nil || n < 1 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
page = n
|
||||
}
|
||||
settings, err := classes.LoadSettings(h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !settings.HTMLEnabled {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
out, found, err := h.Group(r.Context(), settings, r.URL.Query().Get("locale"), r.PathValue("group"), page)
|
||||
if err != nil {
|
||||
log.Error("golem15.sitemap: html group", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
wire.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
7
controllers/api/logger.go
Normal file
7
controllers/api/logger.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "git.golem15.com/golem15/summercms/modules/diary"
|
||||
|
||||
// log is the handlers' logger: its records carry component=sitemap, like
|
||||
// the rest of the plugin (log.components.sitemap).
|
||||
var log = diary.For("sitemap")
|
||||
Reference in New Issue
Block a user