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
This commit is contained in:
65
models/notfound.go
Normal file
65
models/notfound.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// NotFound is one address of the 404 log on golem15_seo_notfound (D-20):
|
||||
// the path (with its query) that answered 404, how often, when first and
|
||||
// last, and the referer of the last hit. The frontend reports each 404
|
||||
// through the authenticated report endpoint.
|
||||
type NotFound struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
URL string `gorm:"column:url"`
|
||||
Hits int64 `gorm:"column:hits"`
|
||||
FirstSeen *time.Time `gorm:"column:first_seen"`
|
||||
LastHit *time.Time `gorm:"column:last_hit"`
|
||||
Referer string `gorm:"column:referer"`
|
||||
}
|
||||
|
||||
func (NotFound) TableName() string { return "golem15_seo_notfound" }
|
||||
|
||||
// MorphName names a 404 log entry in history.
|
||||
func (NotFound) MorphName() string { return `Golem15\SEO\Models\NotFound` }
|
||||
|
||||
// Fillable is empty: entries come from the report endpoint only, and the
|
||||
// preview edits nothing.
|
||||
func (NotFound) Fillable() []string { return []string{} }
|
||||
|
||||
// Rules is empty: nothing is edited.
|
||||
func (NotFound) Rules() map[string]string { return map[string]string{} }
|
||||
|
||||
// The 404 log's status filter scope and its values.
|
||||
const (
|
||||
FilterNotFoundStatus = "FilterNotFoundStatus"
|
||||
NotFoundOpen = "open"
|
||||
NotFoundResolved = "resolved"
|
||||
)
|
||||
|
||||
// FilterScopes lists the 404 log's filter scopes.
|
||||
func (NotFound) FilterScopes() []string { return []string{FilterNotFoundStatus} }
|
||||
|
||||
// coveredSQL is true for an entry an exact redirect or 410 answers now
|
||||
// (the list filter; the Resolved badge also counts patterns and regex).
|
||||
const coveredSQL = `EXISTS (SELECT 1 FROM golem15_seo_redirects AS r WHERE r.match = 'exact' AND (r.source = golem15_seo_notfound.url OR (r.ignore_case AND lower(r.source) = lower(golem15_seo_notfound.url))))`
|
||||
|
||||
// FilterScope narrows the 404 log to open or resolved entries; an unknown
|
||||
// value matches nothing.
|
||||
func (NotFound) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
||||
if db == nil || name != FilterNotFoundStatus {
|
||||
return db
|
||||
}
|
||||
switch s, _ := value.(string); s {
|
||||
case NotFoundOpen:
|
||||
return db.Where("NOT " + coveredSQL)
|
||||
case NotFoundResolved:
|
||||
return db.Where(coveredSQL)
|
||||
}
|
||||
return db.Where("1 = 0")
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(NotFound{})
|
||||
}
|
||||
23
models/notfound/columns.yaml
Normal file
23
models/notfound/columns.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# 404 log columns (UI-SPEC C9).
|
||||
columns:
|
||||
url:
|
||||
label: golem15.seo::lang.notfound.url
|
||||
type: text
|
||||
searchable: true
|
||||
sortable: true
|
||||
hits:
|
||||
label: golem15.seo::lang.notfound.hits
|
||||
type: text
|
||||
sortable: true
|
||||
last_hit:
|
||||
label: golem15.seo::lang.notfound.last_hit
|
||||
type: datetime
|
||||
sortable: true
|
||||
referer:
|
||||
label: golem15.seo::lang.notfound.referer
|
||||
type: text
|
||||
first_seen:
|
||||
label: golem15.seo::lang.notfound.first_seen
|
||||
type: datetime
|
||||
sortable: true
|
||||
invisible: true
|
||||
29
models/notfound/fields.yaml
Normal file
29
models/notfound/fields.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# 404 log preview (UI-SPEC C9): every field is read-only.
|
||||
fields:
|
||||
url:
|
||||
label: golem15.seo::lang.notfound.url
|
||||
type: text
|
||||
span: full
|
||||
context: preview
|
||||
hits:
|
||||
label: golem15.seo::lang.notfound.hits
|
||||
type: number
|
||||
span: left
|
||||
context: preview
|
||||
last_hit:
|
||||
label: golem15.seo::lang.notfound.last_hit
|
||||
type: datepicker
|
||||
mode: datetime
|
||||
span: right
|
||||
context: preview
|
||||
first_seen:
|
||||
label: golem15.seo::lang.notfound.first_seen
|
||||
type: datepicker
|
||||
mode: datetime
|
||||
span: left
|
||||
context: preview
|
||||
referer:
|
||||
label: golem15.seo::lang.notfound.referer
|
||||
type: text
|
||||
span: right
|
||||
context: preview
|
||||
Reference in New Issue
Block a user