feat: graph validation and custom JSON-LD

- seo_schema (seoschema, E8) with the C17 server rules: invalid JSON with
  line and column, non-objects, 16 KB, depth 8, a literal <script and a
  type outside the kind's list refused; the value re-serialised
- a custom node with a generated @id merges into it (schema.merged save
  notice), other nodes are appended to the graph
- seo_primary_parent (seoparent) for article types; both values stored
  in the entry data on every locale row
- the seo_graph widget action (E9 data), P1 primary_gone and the P2
  schema type and breadcrumb trail rows
- GET /_seo/api/v1/graph and MCP seo_get_graph; seo_set_url_seo and
  apply_batch accept primary_parent and schema

The graph tool lands with the validation: the operations parity test
binds the seo_graph widget action to its tool.
This commit is contained in:
Jakub Zych
2026-11-04 14:33:10 +01:00
parent 032d3dae45
commit 17da132b00
19 changed files with 1259 additions and 16 deletions

View File

@@ -83,6 +83,8 @@ type Result struct {
// TypeKey and RecordID name the record behind the URL, "" when none.
TypeKey string `json:"-"`
RecordID string `json:"-"`
// Merged names the generated values custom JSON-LD replaced.
Merged []schema.Merged `json:"-"`
}
// SEO is the head metadata of a resolved page.
@@ -157,6 +159,9 @@ type Resolver struct {
// non-production lock (D-22) is not applied. The sitemap filter decides
// on these, so staging lists the same addresses as production.
production bool
// schema replaces the stored seo_schema value of the page when set
// (the graph preview of unsaved changes and the MCP graph tool).
schema *string
}
// Resolver returns the service's resolver.
@@ -799,6 +804,7 @@ func (r *Resolver) resolveAt(ctx context.Context, db *gorm.DB, settings models.S
if err := svc.structuredData(ctx, db, settings, &res, structuredIn{
key: key, id: res.RecordID, rec: rec, entry: entry, content: content,
locale: locale, path: path, url: self, home: home, siteName: svc.SiteName(ctx, settings, locale), seo: seo,
schemaValue: r.schema, sources: src,
}); err != nil {
return Result{}, err
}