docs: structured data, breadcrumbs, hreflang and pagination in README
This commit is contained in:
50
README.md
50
README.md
@@ -38,7 +38,10 @@ The frontend (a Nuxt layer or app) asks `GET /_seo/api/v1/resolve?url=...` for t
|
||||
- **CSV import and export** of redirects, URL rules and the SEO of records (by URL), in the **Import and export** screen (element `golem15-seo-transfer`) and through MCP. Files are UTF-8 with a BOM, comma or semicolon; exported cells that start a formula (`=`, `+`, `-`, `@`, tab, CR), or apostrophes followed by one, are prefixed with an apostrophe, and the import drops exactly that one again, so a re-imported export is unchanged. An import is previewed row by row (new, changed, unchanged, refused, with notes) and applied as one batch with via `import`, or not at all while a row is refused. The screen's apply token is an HMAC-SHA256 under the app key over the file and a fingerprint of the data the preview read; it is refused with `transfer.stale` when the data changed. The screen previews files up to 60 KB and downloads exports up to 250 KB; MCP imports take up to 900 KB per call and exports come in pages of 1000 rows.
|
||||
- **Record events.** A deleted record loses its entries after commit; a created record gets its address snapshot; an updated record whose address changed gets its 301.
|
||||
- **Entry generation.** The `golem15.seo.generate_entries` job (conga) creates missing entries with the current address of every published record and refreshes the effective-value cache; a record whose address changed without a record event gets its address change (with its 301) like an admin save. Editor values are never changed.
|
||||
- **Field types** `seotext`, `mlseotext`, `seorobots`, `mlseorobots`, `seoimage` and `robotstxt`, with the custom elements `golem15-seo-text`, `golem15-seo-robots`, `golem15-seo-serp` (the result preview widget), `golem15-seo-image`, `golem15-seo-robotstxt` (the robots.txt editor), `golem15-seo-robotstester` (the tester widget) and `golem15-seo-transfer` (the import and export widget).
|
||||
- **Field types** `seotext`, `mlseotext`, `seorobots`, `mlseorobots`, `seoimage`, `robotstxt`, `seoschema` and `seoparent`, with the custom elements `golem15-seo-text`, `golem15-seo-robots`, `golem15-seo-serp` (the result preview widget), `golem15-seo-image`, `golem15-seo-robotstxt` (the robots.txt editor), `golem15-seo-robotstester` (the tester widget), `golem15-seo-transfer` (the import and export widget), `golem15-seo-schema` (structured data), `golem15-seo-graph` (the structured data preview widget) and `golem15-seo-parent` (the primary category).
|
||||
- **Structured data.** Every `ok` resolve answer carries one JSON-LD `@graph` built from the content descriptor and the settings: the page node (WebPage or its subtype), the article with its authors, dates, images, word count and section, the person of a profile page, a Service with its provider and offer, the BreadcrumbList, one VideoObject per YouTube video, the WebSite (with a SearchAction when `search_url` is set) and the publisher. Editors pick the schema type per page, fill the Service fields and add custom JSON-LD; the SEO tab previews the graph with Google's required and recommended properties and links the Rich Results Test.
|
||||
- **Publisher and local business** settings: an organisation or a person with logo, website, contact and sameAs profiles, optionally a local business with address, coordinates, opening hours and price range.
|
||||
- **Breadcrumbs, hreflang and pagination.** The resolve answer carries the breadcrumb trail (through the record's primary category), the hreflang alternates with `x-default`, `html_lang` and `og:locale`, and the page of a paginated address in the site's form (`?page=N` or `/<segment>/N`).
|
||||
- **One permission**, `golem15.seo.manage`, for the SEO tab, rules, redirects, the 404 log, robots.txt, settings and history, in the admin and through MCP.
|
||||
- **Management API and MCP.** Every SEO admin operation has a JSON route under `/_seo/api/v1` and an MCP tool bound to it (pier, prefix `seo_`). Tools address pages by URL: a change goes to the record behind the URL, else to an exact URL rule, and the answer says where. A read grant sees only the read tools. `AdminOperations` maps each admin operation to its tool, and a test fails when one has none.
|
||||
- **Issue report.** Missing and duplicate titles and descriptions, URL rules overriding a record, redirect chains, canonicals pointing to a noindex or redirected address, noindex addresses robots.txt blocks, and sitemap addresses the production robots.txt keeps Googlebot from (`sitemap_blocked`, severity `error`, with the deciding line), each with the tool that fixes it.
|
||||
@@ -124,6 +127,36 @@ The pipeline runs normalisation, then redirects on the normalised path (followed
|
||||
|
||||
`status` is 302 when a temporary redirect takes part. A 410 rule answers `"action": "gone"`, `"status": 410`. A redirect cycle met at runtime (stored past the save check) stops after 10 hops with `not_found` and is logged at most once an hour per cycle (keyed by its rules, not by the requested URL). A path that no published record owns and no URL rule covers answers `"action": "not_found"`, `"status": 404` and `"seo": null` (the home page `/` is always known). A `url` that is neither a path nor an http(s) URL is a `422`. Absolute URLs are built from `golem15.seo.site_url`, or from the request URL when it names the site's host or its `www` twin (for the scheme and host rules), never from the request's Host header.
|
||||
|
||||
### Structured data, breadcrumbs, hreflang and pagination
|
||||
|
||||
An `ok` resolve answer also carries (all additive, absent on other actions):
|
||||
|
||||
```json
|
||||
{
|
||||
"html_lang": "en",
|
||||
"alternates": [
|
||||
{"hreflang": "en", "url": "https://example.com/blog/first-post"},
|
||||
{"hreflang": "pl", "url": "https://example.com/pl/blog/pierwszy-wpis"},
|
||||
{"hreflang": "x-default", "url": "https://example.com/blog/first-post"}
|
||||
],
|
||||
"breadcrumbs": [
|
||||
{"name": "Home", "url": "https://example.com/", "path": "/"},
|
||||
{"name": "Recipes", "url": "https://example.com/blog/category/recipes", "path": "/blog/category/recipes"},
|
||||
{"name": "First post", "url": "https://example.com/blog/first-post", "path": "/blog/first-post"}
|
||||
],
|
||||
"jsonld": {"@context": "https://schema.org", "@graph": ["…"]},
|
||||
"seo": {"og": {"locale": "en_US", "locale_alternates": ["pl_PL"]}}
|
||||
}
|
||||
```
|
||||
|
||||
- **The graph.** Nodes link by `@id`: `<site>/#website`, `<site>/#organization` or `<site>/#person` (the publisher), `<url>#webpage`, `<url>#breadcrumb`, `<url>#article`, `<url>#profile`, `<url>#service`, `<url>#video-<n>`; a collection page's `mainEntity` is `<url>#itemlist`, the node the frontend adds with the listed items. The type follows the descriptor's `Kind`: article → BlogPosting (Article or NewsArticle by choice), collection → CollectionPage, profile → ProfilePage with a Person, page → WebPage (AboutPage, ContactPage or Service by choice; registry pages of kind offer or service default to Service). No e-commerce type is generated.
|
||||
- **Custom JSON-LD** (the SEO tab's Structured data field, or `schema` on `seo_set_url_seo`): an object or a list of objects, at most 16 KB and 8 levels deep, without a `<script`; the server decodes it and stores it re-serialised. A node with a generated `@id` is merged into that node with the custom values first (the save answers the `schema.merged` notice); other nodes are added.
|
||||
- **Breadcrumbs** start with the home crumb (setting **Home page name in breadcrumbs**, default Home / Strona główna), follow the record's primary category (the SEO tab's Primary category, else the first of the descriptor's `Parents`) and each parent's own primary parent up to 6 levels, leaving out unpublished parents, and end with the page. The BreadcrumbList node has the same items; the home page's trail is the home crumb alone.
|
||||
- **hreflang** lists every locale the record has an address in, the page itself included, plus `x-default` on the default locale's address; a record in one locale has none. `og:locale` comes from `golem15.seo.og_locales`, else `pl_PL`, `en_US`, `de_DE`, else the language with its region.
|
||||
- **Pagination** (settings tab Pagination): the form is the query (`?page=N`, default) or the path (`/<segment>/N`, the segment per locale, default `strona` for pl and `page` for en). `?page=1` and `/<segment>/1` answer 301 to the unnumbered address, the other form 301s to the configured one, together with the address rules in one hop. A page value that is not a number answers `not_found` with `"pagination": {"invalid": true, …}`, so the frontend answers 404. Page 2 and later answers `ok` with its own canonical, `index, follow`, the title suffix after the page title and the description suffix at the end; `%page%` is filled and `%pages%` is left for the frontend, which knows the page count. Such an answer carries `"pagination": {"form", "param", "segment", "page", "invalid"}`.
|
||||
|
||||
`GET /_seo/api/v1/site` also answers `"pagination": {"form", "param", "segments"}` and `"home_crumb": {"name", "url", "names"}` (the name in the default locale and per locale).
|
||||
|
||||
### The 404 report endpoint
|
||||
|
||||
`POST /_seo/api/v1/notfound` takes `{"url": "/missing", "referer": "https://..."}` from the frontend server with `Authorization: Bearer <golem15.seo.notfound_token>` (compared in constant time) and answers `204`. A missing or wrong token is `401`; with no token configured the endpoint is off and answers `404`. URLs are capped at 2048 bytes (`422` beyond) and referers at 1024. The log holds at most `golem15.seo.notfound_max_rows` addresses; new ones beyond it are dropped with one warning an hour. Throttle bucket `seo-notfound`, 600 requests a minute per client IP.
|
||||
@@ -185,12 +218,13 @@ The management routes live under `/_seo/api/v1` (bucket `seo-api`, 300 requests
|
||||
| Tool (`seo_` + name) | Route | What it does |
|
||||
|------|-------|--------------|
|
||||
| `get_url_seo` | `GET /seo?url=` | Effective SEO of a URL with the source of every value, the redirect decision, the owner and the SEO tab's warnings. |
|
||||
| `set_url_seo` | `PUT /seo` | Values for a URL: the record behind it in the URL's locale, else an exact URL rule; `force_rule` writes a rule. Answers `written_to` (`record` or `rule`), `type`, `id`, `locale`, `rule_id`. |
|
||||
| `set_url_seo` | `PUT /seo` | Values for a URL: the record behind it in the URL's locale, else an exact URL rule; `force_rule` writes a rule. `primary_parent` (`<type>:<id>`) and `schema` (the structured data JSON string) are records only and shared by every locale. Answers `written_to` (`record` or `rule`), `type`, `id`, `locale`, `rule_id`. |
|
||||
| `get_graph` | `GET /graph?url=&schema=` | The JSON-LD graph of a URL and every node's missing required and recommended properties (`rules` false for a type Google has no rules for); `schema` previews an unsaved value. Answers `{locale, url, graph, nodes}`. |
|
||||
| `list_rules`, `show_rule`, `create_rule`, `update_rule`, `delete_rule` | `GET/POST /rules`, `GET/PUT/DELETE /rules/{id}` | URL rules. |
|
||||
| `list_redirects`, `show_redirect`, `create_redirect`, `update_redirect`, `delete_redirect` | `GET/POST /redirects`, `GET/PUT/DELETE /redirects/{id}` | Redirects; a chain is saved with a warning. |
|
||||
| `list_not_found`, `redirect_not_found`, `gone_not_found`, `delete_not_found` | `GET /notfound`, `POST /notfound/{id}/redirect`, `POST /notfound/{id}/gone`, `DELETE /notfound/{id}` | The 404 log and its actions; an address a rule answers is `409`. |
|
||||
| `show_robots`, `update_robots`, `test_robots` | `GET/PUT /robots`, `GET /robots/test?url=&agent=` | robots.txt, the AI bot switches, `llms_enabled` and `llms_intro`, and the tester on the saved file. |
|
||||
| `show_settings`, `update_settings` | `GET/PUT /settings` | The SEO settings (including `links_new_tab`, `nofollow_domains`, `sponsored_domains`) and, under `types`, the per-type settings. |
|
||||
| `show_settings`, `update_settings` | `GET/PUT /settings` | The SEO settings (including `links_new_tab`, `nofollow_domains`, `sponsored_domains`, `breadcrumb_home`, the `page_*` pagination keys, the `publisher_*` and `local_*` keys) and, under `types`, the per-type settings. |
|
||||
| `list_history`, `show_history` | `GET /history`, `GET /history/{id}` | History, filtered by `via`, `kind` and `batch`. |
|
||||
| `undo_change`, `undo_batch` | `POST /history/{id}/undo`, `POST /history/batches/{batch}/undo` | Undo; refusals are `409` with `undo_stale`, `undo_loop`, `undo_gone` or `already_undone`. |
|
||||
| `apply_batch` | `POST /batch` | Changes (`set_seo`, `create_redirect`, `update_redirect`, `delete_redirect`, `create_rule`, `update_rule`, `delete_rule`) all or nothing; `dry_run` answers the diff and the conflicts. A refused change is `422` (`redirect_loop` with the change and the path, or `batch_refused`). |
|
||||
@@ -204,7 +238,8 @@ Lists take `page`, `per_page` (at most 200) and `search`, and answer `{"data": [
|
||||
### Admin
|
||||
|
||||
- Navigation item **SEO** (icon `search`), landing on **Redirects**, with the side menu **Redirects**, **404 log**, **URL rules**, **Pages**, **robots.txt**, **History** and **Import and export**.
|
||||
- The SEO settings have a **Links** tab (after Addresses): the new-tab switch and the nofollow and sponsored domain lists, one domain per line (`settings.domain_invalid` names a bad line).
|
||||
- The SEO settings have a **Pagination** tab (after Addresses), a **Links** tab (the new-tab switch and the nofollow and sponsored domain lists, one domain per line; `settings.domain_invalid` names a bad line), a **Publisher** tab with the generated publisher node and its findings, and a **Local business** tab. A person cannot be a local business (`settings.local_person`).
|
||||
- The SEO tab adds **Primary category** (article types), **Structured data** and its **preview** after the social image; its status warns when a stored primary category is no longer assigned, and the effective values show the schema type and the breadcrumb trail.
|
||||
- Settings tile **SEO settings** (`seo_settings`), a link to the settings singleton controller.
|
||||
- Every SEO screen and the SEO tab require `golem15.seo.manage`. Without it an administrator sees no SEO tab, and a posted SEO value changes nothing.
|
||||
|
||||
@@ -226,6 +261,11 @@ The element bundle (`assets/dist/seo.js` and `seo.css`) is built from `assets/sr
|
||||
| `classes.PageWrite`, `classes.RouteReport`, `classes.ValidPagePath`, `classes.PageMissing` | A registry write, a route report's outcome, the address check and the missing state. Service: `CheckPage`, `SavePage`, `DeletePage`, `ApplyPageTexts`, `ReportRoutes`, `RouteReportToken`, `PageAt`. |
|
||||
| `classes.SiteAnswer` | `Service.Site`, the site answer. `Service.LLMSText` builds llms.txt. |
|
||||
| `classes.Outbound`, `classes.OutboundOf` | The resolve answer's `outbound` from the settings. |
|
||||
| `schema.Build`, `schema.Input`, `schema.Validate`, `schema.Normalize`, `schema.DecodeCustom`, `schema.Merge` | Package `classes/schema`: the JSON-LD graph of a page, Google's required and recommended properties per type, the structured data value and custom JSON-LD. |
|
||||
| `classes.Alternate`, `classes.Alternates`, `classes.LangTag` | hreflang alternates and language tags; `Service.OGLocale`. |
|
||||
| `classes.Pagination`, `classes.PageConfig` | The resolve answer's pagination block and a site's page form (`Service.PageConfigOf`). |
|
||||
| `classes.GraphAnswer` | `Service.GraphOfURL` and `Service.GraphOfRecord`, the graph and its findings; `classes.RecordGraphAction` is the SEO tab's preview action. |
|
||||
| `classes.PrimaryParent`, `classes.ParentOption` | A record's primary category and its choices (`Service.ParentOptions`); `Service.Trail` builds the breadcrumb trail, `Service.PublisherOf` the publisher. |
|
||||
| `classes.SitemapExclusion` | `Service.RecordSitemapExclusions`, why the sitemaps leave out a record's addresses; `Service.SitemapListed` reads the listed addresses; `Service.SitemapLines` the robots.txt `Sitemap:` addresses. |
|
||||
| `classes.SiteCacheKind` | `seo`, the kind of the site-cache purge after an SEO write. |
|
||||
| `classes.IssueSitemapBlocked`, `classes.SeverityError` | The `sitemap_blocked` issue kind and its severity. |
|
||||
@@ -261,6 +301,8 @@ The element bundle (`assets/dist/seo.js` and `seo.css`) is built from `assets/sr
|
||||
| `golem15.seo.notfound_token` | `""` | The bearer token of `POST /_seo/api/v1/notfound`; empty turns the endpoint off. |
|
||||
| `golem15.seo.notfound_max_rows` | `10000` | The most addresses the 404 log holds. |
|
||||
| `golem15.seo.robots_sitemaps` | unset | The sitemap addresses robots.txt names, one `Sitemap:` line each, absolute on the site URL. Unset, robots.txt names the sitemap plugin's index. |
|
||||
| `golem15.seo.og_locales` | unset | Locale code to `og:locale` value, for example `{pl: pl_PL, en: en_GB}`. Unlisted codes use `pl_PL`, `en_US`, `de_DE`, else the language with its region. |
|
||||
| `golem15.seo.search_url` | unset | The site search address with `{search_term_string}` (`/szukaj?q={search_term_string}`); set, the WebSite node carries a SearchAction. |
|
||||
| `golem15.seo.route_report_token` | `""` | The bearer token of `POST /_seo/api/v1/registry/routes`; empty uses `notfound_token`, and both empty turn the route report off. |
|
||||
|
||||
The non-production lock reads the application environment (`SUMMER_ENV`); it has no key of its own. The address rules (host, trailing slash, lowercase) are settings, on the Addresses tab.
|
||||
|
||||
Reference in New Issue
Block a user