feat: page registry with its admin screen and signpost type

- golem15_seo_pages (migration 202610280002): the site's own frontend pages
  with an address and a name per locale, kind, origin, parent, content date
  and note; settings routes_reported_at
- SEO > Pages (golem15.seo.pages) after URL rules: list with kind, origin
  and missing filters and the missing row state, form with the P9 partial;
  the plugin's form extension saves the address and name per locale and
  refuses url_required, url_invalid, url_taken and url_owned
- menu item type seo-page with a content descriptor, so each page gets
  the SEO tab, resolves ok and is listed in the sitemaps grouped by kind
  (pages, offers, services, landing-pages, titled per locale)
- management API GET/POST /_seo/api/v1/pages, PUT/DELETE /pages/{id} and
  the MCP tools seo_list_pages, seo_create_page, seo_update_page,
  seo_delete_page (in this commit because the operations parity test maps
  every Pages operation to its tool)
This commit is contained in:
Jakub Zych
2026-11-04 13:20:45 +01:00
parent 17b810dfde
commit 9e802ec850
27 changed files with 2281 additions and 9 deletions

View File

@@ -23,7 +23,10 @@ func (p *Plugin) AdminActionExtensions() []pact.AdminActionExtension {
if reg == nil {
return nil
}
var out []pact.AdminActionExtension
// The Pages form's address and name come first: the SEO tab of a
// registry page (added below, the registry being a content type) reads
// the address its save wrote.
out := []pact.AdminActionExtension{{Controller: controllers.PagesControllerID, Form: p.pagesForm()}}
types := reg.ContentTypes()
for _, typ := range types {
if typ.Content == nil || strings.TrimSpace(typ.Content.Controller) == "" {