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:
@@ -22,6 +22,30 @@ var EntryColumns = []string{
|
||||
// every locale row (Q-5): the social image.
|
||||
const SharedColumn = "og_image"
|
||||
|
||||
// DataColumns are the SEO tab values stored in an entry's data (P33-23):
|
||||
// the primary parent and the structured data value. Like the social image
|
||||
// they are one per record, written on every locale row.
|
||||
var DataColumns = []string{DataPrimaryParent, DataSchema}
|
||||
|
||||
// sharedColumns are every value shared by the locale rows of a record.
|
||||
var sharedColumns = append([]string{SharedColumn}, DataColumns...)
|
||||
|
||||
// SharedData returns a record's shared data value: the first non-empty
|
||||
// value in locale order.
|
||||
func SharedData(entries map[string]models.Entry, key string) string {
|
||||
locales := make([]string, 0, len(entries))
|
||||
for locale := range entries {
|
||||
locales = append(locales, locale)
|
||||
}
|
||||
sort.Strings(locales)
|
||||
for _, locale := range locales {
|
||||
if v := entryData(entries[locale], key); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SharedValue returns a record's shared social image: the first non-empty
|
||||
// value in locale order.
|
||||
func SharedValue(entries map[string]models.Entry) string {
|
||||
@@ -61,6 +85,8 @@ func entryValue(e models.Entry, column string) string {
|
||||
return e.OGDescription
|
||||
case "og_image":
|
||||
return e.OGImage
|
||||
case DataPrimaryParent, DataSchema:
|
||||
return entryData(e, column)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -85,6 +111,17 @@ func setEntryValue(e *models.Entry, column, value string) {
|
||||
e.OGDescription = value
|
||||
case "og_image":
|
||||
e.OGImage = value
|
||||
case DataPrimaryParent, DataSchema:
|
||||
data := models.JSONMap{}
|
||||
for k, v := range e.Data {
|
||||
data[k] = v
|
||||
}
|
||||
if value == "" {
|
||||
delete(data, column)
|
||||
} else {
|
||||
data[column] = value
|
||||
}
|
||||
e.Data = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +189,11 @@ func (s *Service) SaveRecord(ctx context.Context, db *gorm.DB, in RecordWrite) (
|
||||
}
|
||||
described := s.recordLocales(ctx, in.Type.Key, in.ID)
|
||||
values := in.Values
|
||||
if shared, ok := values[SharedColumn]; ok {
|
||||
for _, column := range sharedColumns {
|
||||
shared, ok := values[column]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// One value for every locale of the record (Q-5).
|
||||
v := ""
|
||||
for _, text := range shared {
|
||||
@@ -166,10 +207,10 @@ func (s *Service) SaveRecord(ctx context.Context, db *gorm.DB, in RecordWrite) (
|
||||
all[locale] = v
|
||||
}
|
||||
copied := RecordValues{}
|
||||
for column, perLocale := range values {
|
||||
copied[column] = perLocale
|
||||
for c, perLocale := range values {
|
||||
copied[c] = perLocale
|
||||
}
|
||||
copied[SharedColumn] = all
|
||||
copied[column] = all
|
||||
values = copied
|
||||
}
|
||||
touched := map[string]models.Entry{}
|
||||
|
||||
Reference in New Issue
Block a user