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

@@ -36,6 +36,12 @@ field:
og_title: Social title
og_description: Social description
og_image: Social image
primary_category: "Primary category (breadcrumb trail)"
primary_category_comment: "Sets the breadcrumb trail and the article section. Default: the first category."
primary_category_default: "Default (first category)"
schema: "Structured data"
schema_comment: "The schema.org type of this page in its JSON-LD graph. Shared by all languages."
graph: "Structured data preview"
robots:
llms_enabled: Serve llms.txt
llms_enabled_comment: Lists the site's indexable pages from the sitemaps at /llms.txt, for AI tools that read it.
@@ -73,6 +79,7 @@ status:
canonical_noindex: The canonical URL “:url” is set to noindex, so search engines may ignore both pages.
canonical_redirected: The canonical URL “:url” redirects to “:target”. Point the canonical to the final address.
staging: On this environment every page is served with noindex and robots.txt blocks everything (non-production lock).
primary_gone: "The primary category “:name” is no longer assigned, so the first category is used. Choose another one below."
settings:
placeholders: Placeholders
placeholders_code: Placeholder
@@ -317,6 +324,8 @@ effective:
source_content: Content
source_default: Site default
no_url: This language has no address yet. Fill in its slug and save.
schema: "Schema type"
trail: "Breadcrumb trail"
placeholder:
title: The content's title
description: The content's own description or excerpt
@@ -422,6 +431,7 @@ api:
change_not_found: The record this change names does not exist.
op_unknown: "Unknown operation “:op”."
conflict_noindex_published: "“:url” is a published page listed in the sitemaps; noindex removes it from search results."
record_only: "Only the SEO of a record has this value."
transfer:
title: Import and export
delimiter: CSV delimiter
@@ -497,3 +507,16 @@ graph:
recommended: "Missing recommended property “:property” (:type)"
no_rules: ":type has no rich-result rules at Google; it is checked as valid schema.org only."
all_good: "No problems: every node has its required and recommended properties."
schema:
invalid_json: "The custom JSON-LD is not valid JSON: :error"
not_object: "The custom JSON-LD must be an object or a list of objects."
too_large: "The custom JSON-LD may be at most 16 KB."
too_deep: "The custom JSON-LD is nested more than 8 levels deep."
script_tag: "Remove the script tag and paste only the JSON."
type_unknown: "This schema type is not available for :type."
field_invalid: ":field: :message"
field_price: "Price"
field_currency: "Currency"
price_invalid: "Enter a price such as 150 or 149.99."
currency_invalid: "Use a three-letter currency code, e.g. PLN."
merged: "Saved. Your custom JSON-LD replaces :props in the generated :node node."

View File

@@ -36,6 +36,12 @@ field:
og_title: Tytuł w mediach społecznościowych
og_description: Opis w mediach społecznościowych
og_image: Obraz w mediach społecznościowych
primary_category: "Kategoria główna (ścieżka nawigacji)"
primary_category_comment: "Ustala ścieżkę nawigacji i sekcję artykułu. Domyślnie: pierwsza kategoria."
primary_category_default: "Domyślna (pierwsza kategoria)"
schema: "Dane strukturalne"
schema_comment: "Typ schema.org tej strony w jej grafie JSON-LD. Wspólny dla wszystkich wersji językowych."
graph: "Podgląd danych strukturalnych"
robots:
llms_enabled: Udostępniaj llms.txt
llms_enabled_comment: Wypisuje indeksowalne strony witryny z map witryny pod /llms.txt, dla narzędzi AI, które go czytają.
@@ -75,6 +81,7 @@ status:
canonical_noindex: Adres kanoniczny „:url” ma ustawione noindex, więc wyszukiwarki mogą zignorować obie strony.
canonical_redirected: Adres kanoniczny „:url” przekierowuje na „:target”. Ustaw adres kanoniczny na adres docelowy.
staging: W tym środowisku każda strona ma noindex, a robots.txt blokuje wszystko (blokada środowiska nieprodukcyjnego).
primary_gone: "Kategoria główna „:name” nie jest już przypisana, więc używana jest pierwsza kategoria. Wybierz inną poniżej."
settings:
placeholders: Znaczniki
placeholders_code: Znacznik
@@ -323,6 +330,8 @@ effective:
source_content: Treść
source_default: Domyślne dla witryny
no_url: Ta wersja językowa nie ma jeszcze adresu. Uzupełnij jej slug i zapisz.
schema: "Typ schema"
trail: "Ścieżka nawigacji"
placeholder:
title: Tytuł treści
description: Własny opis lub zajawka treści
@@ -432,6 +441,7 @@ api:
change_not_found: Rekord wskazany w tej zmianie nie istnieje.
op_unknown: "Nieznana operacja „:op”."
conflict_noindex_published: "„:url” to opublikowana strona z map witryny; noindex usunie ją z wyników wyszukiwania."
record_only: "Tę wartość ma tylko SEO rekordu."
transfer:
title: Import i eksport
delimiter: Separator CSV
@@ -511,3 +521,16 @@ graph:
recommended: "Brak zalecanej właściwości „:property” (:type)"
no_rules: ":type nie ma reguł wyników z elementami rozszerzonymi w Google; sprawdzana jest tylko poprawność schema.org."
all_good: "Brak problemów: każdy węzeł ma wymagane i zalecane właściwości."
schema:
invalid_json: "Własny JSON-LD nie jest poprawnym JSON-em: :error"
not_object: "Własny JSON-LD musi być obiektem lub listą obiektów."
too_large: "Własny JSON-LD może mieć najwyżej 16 KB."
too_deep: "Własny JSON-LD jest zagnieżdżony głębiej niż 8 poziomów."
script_tag: "Usuń znacznik script i wklej sam JSON."
type_unknown: "Ten typ schema jest niedostępny dla: :type."
field_invalid: ":field: :message"
field_price: "Cena"
field_currency: "Waluta"
price_invalid: "Wpisz cenę, np. 150 lub 149,99."
currency_invalid: "Użyj trzyliterowego kodu waluty, np. PLN."
merged: "Zapisano. Twój JSON-LD zastępuje :props w wygenerowanym węźle :node."