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

@@ -0,0 +1,56 @@
package updates
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
// createPages creates golem15_seo_pages, the page registry (D-33): the
// site's own frontend pages with their address per locale (url is the
// default locale's, urls the others'), name per locale, kind, origin,
// parent, content date, last report and note, one row per default-locale
// address. The settings get routes_reported_at, the time of the newest
// route report, against which a reported page shows as missing.
var createPages = []*gormigrate.Migration{
{
ID: "202610280002_create_golem15_seo_pages",
Migrate: func(tx *gorm.DB) error {
if err := tx.Exec(`CREATE TABLE golem15_seo_pages (
id BIGSERIAL PRIMARY KEY,
url TEXT NOT NULL,
urls JSONB NOT NULL DEFAULT '{}'::jsonb,
title TEXT NOT NULL DEFAULT '',
titles JSONB NOT NULL DEFAULT '{}'::jsonb,
kind TEXT NOT NULL DEFAULT 'page',
origin TEXT NOT NULL DEFAULT 'manual',
parent_id BIGINT NULL REFERENCES golem15_seo_pages (id) ON DELETE SET NULL,
content_updated_at DATE NULL,
reported_at TIMESTAMPTZ NULL,
note TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP(0) NULL,
updated_at TIMESTAMP(0) NULL
)`).Error; err != nil {
return err
}
// One page per default-locale address; a row being created holds
// '' only inside its own save transaction.
if err := tx.Exec(`CREATE UNIQUE INDEX golem15_seo_pages_url ON golem15_seo_pages (url) WHERE url <> ''`).Error; err != nil {
return err
}
if err := tx.Exec(`CREATE INDEX golem15_seo_pages_urls ON golem15_seo_pages USING gin (urls jsonb_path_ops)`).Error; err != nil {
return err
}
return tx.Exec(`ALTER TABLE golem15_seo_settings ADD COLUMN routes_reported_at TIMESTAMPTZ NULL`).Error
},
Rollback: func(tx *gorm.DB) error {
if err := tx.Exec(`ALTER TABLE golem15_seo_settings DROP COLUMN IF EXISTS routes_reported_at`).Error; err != nil {
return err
}
return tx.Exec(`DROP TABLE IF EXISTS golem15_seo_pages`).Error
},
},
}
func init() {
Register(createPages...)
}