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:
2026-10-27 11:35:25 +01:00
parent 163b0aaf98
commit 6eb6cb7a4d
18 changed files with 997 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
{{- with .Data.Text -}}
<div class="summer-callout" role="status">
<p class="summer-callout__text">{{ . }}</p>
</div>
{{- end -}}

View File

@@ -0,0 +1,11 @@
# 404 log filters (UI-SPEC C9): open or resolved, and the last hit.
scopes:
status:
label: golem15.seo::lang.notfound.filter_status
modelClass: Golem15\SEO\Models\NotFound
nameFrom: url
scope: FilterNotFoundStatus
last_hit:
label: golem15.seo::lang.notfound.last_hit
type: daterange
column: last_hit

View File

@@ -0,0 +1,14 @@
# 404 log preview (UI-SPEC C9): read-only, the resolved hint above the card
# and the two actions that apply while the entry is open.
name: golem15.seo::lang.notfound.name
form: ~/plugins/golem15/seo/models/notfound/fields.yaml
modelClass: Golem15\SEO\Models\NotFound
defaultRedirect: golem15/seo/notfound
preview:
headerPartial: hint
recordActions: [redirect, gone]
messages:
preview: golem15.seo::lang.notfound.preview

View File

@@ -0,0 +1,25 @@
# 404 log (UI-SPEC C9): most hits first, resolved entries badged, no create.
list: ~/plugins/golem15/seo/models/notfound/columns.yaml
modelClass: Golem15\SEO\Models\NotFound
title: golem15.seo::lang.notfound.list_title
recordUrl: golem15/seo/notfound/preview/:id
noRecordsMessage: golem15.seo::lang.notfound.list_empty
recordsPerPage: 50
showCheckboxes: true
showSetup: true
filter: config_filter.yaml
bulkActions: [gone]
defaultSort:
column: hits
direction: desc
toolbar:
buttons: [delete]
search:
prompt: backend::lang.list.search_prompt
messages:
empty: golem15.seo::lang.notfound.list_empty
deleteConfirm: golem15.seo::lang.notfound.delete_selected_confirm
rowStateDisabled: golem15.seo::lang.notfound.state_resolved