Files
sm-seo-plugin/routes.go
Jakub Zych 6eb6cb7a4d feat: 404 log and report endpoint
- POST /_seo/api/v1/notfound with the golem15.seo.notfound_token bearer
  (constant-time, off when unset), URL and referer caps, a row cap with
  an hourly warning, throttle bucket seo-notfound
- golem15.seo.notfound list by hits with Resolved rows, a preview with the
  resolved hint, a prompted 301 and a 410 record action, a bulk 410 that
  refuses a selection with a covered address
- every /_seo/api/v1 answer carries X-Robots-Tag: noindex
2026-10-27 11:35:25 +01:00

63 lines
1.9 KiB
Go

package seo
import (
"net/http"
"time"
apicontrollers "git.golem15.com/golem15/sm-seo-plugin/controllers/api"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/surf"
)
// The throttle buckets of the public endpoints.
const (
// BucketResolve is the resolve endpoint's bucket.
BucketResolve = "seo-resolve"
// BucketNotFound is the 404 report endpoint's bucket.
BucketNotFound = "seo-notfound"
)
// Routes mounts the /_seo/api/v1 endpoints. They carry no backend
// middleware: the frontend server calls them anonymously (the 404 report
// with its own token). Every answer carries X-Robots-Tag: noindex.
func (p *Plugin) Routes(r pact.Router) error {
r.Group("/_seo/api/v1", nil, func(g pact.Router) {
g.Get("/resolve", noindex(apicontrollers.Resolve(p.Service)), "throttle:"+BucketResolve)
g.Post("/notfound", noindex(apicontrollers.NotFound(p.Service)), "throttle:"+BucketNotFound)
})
return nil
}
// noindex adds X-Robots-Tag: noindex to every answer of h.
func noindex(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Robots-Tag", "noindex")
h(w, r)
}
}
// Buckets registers the per-client-IP buckets behind the configured
// trusted proxies: seo-resolve 600 a minute (the frontend server resolves
// every page it renders) and seo-notfound 600 a minute (one report per
// 404 page).
func (p *Plugin) Buckets() map[string]surf.Bucket {
trusted := surf.TrustedProxies(nil)
if p.app != nil {
trusted = surf.TrustedProxies(p.app.Config)
}
bucket := func(name string, max int) surf.Bucket {
return surf.Bucket{
Name: name,
Max: max,
Decay: time.Minute,
Key: func(r *http.Request) string {
return name + "|" + surf.ClientIP(r, trusted)
},
}
}
return map[string]surf.Bucket{
BucketResolve: bucket(BucketResolve, 600),
BucketNotFound: bucket(BucketNotFound, 600),
}
}