diff --git a/.planning/phases/32-seo-foundation/32-UI-SPEC.md b/.planning/phases/32-seo-foundation/32-UI-SPEC.md
new file mode 100644
index 00000000..228af890
--- /dev/null
+++ b/.planning/phases/32-seo-foundation/32-UI-SPEC.md
@@ -0,0 +1,953 @@
+---
+phase: "32"
+slug: "seo-foundation"
+status: approved
+reviewed_at: "2026-10-26"
+shadcn_initialized: false
+preset: none
+created: "2026-10-26"
+---
+
+# Phase 32 — UI Design Contract
+
+> Visual and interaction contract for frontend phases. Generated by msd-ui-researcher, verified by msd-ui-checker.
+
+Phase 32 adds **admin surfaces only**. The public rendering (head tags, JSON-LD, breadcrumbs, the Nuxt SEO layer) is Phase 33 and has no contract here. Every surface below belongs to `sm-seo-plugin` (Golem15.SEO) and is rendered through generic cabana/boardwalk mechanisms. The framework (`summercms.go`) gains no SEO-named UI, phrase, icon or component (D-90).
+
+The contract fixes six things:
+
+1. **Where SEO lives in the admin**: one "SEO" navigation item with six side-menu screens, one Settings tile, and the SEO tab that D-91 adds to every registered content type's form.
+2. **What the YAML declares**: fields, tabs, columns, filters, record and bulk actions, written as ordinary `fields.yaml` / `columns.yaml` / `config_*.yaml`.
+3. **Six plugin custom elements** for the needs no built-in field type covers (text with counters, robots directives, the result preview, the robots.txt editor, the robots tester, CSV import and export). Each is named, and each says why a built-in type is not enough.
+4. **Five server-rendered partials** that use only the existing partial kit (`.summer-callout`, `.summer-table`, `.summer-markdown`).
+5. **All copy**, in English and Polish.
+6. **Framework-visible rendering rules for D-91**, so the SPA renders a compiled extended form without reordering it.
+
+The SPA has a complete design system: Direction C v2 (`.planning/phases/10-admin-vue-spa/design/README.md`), implemented as Tailwind 4 `@theme` tokens in `admin/src/styles/main.css` on Reka UI primitives. **This contract adds no token, colour variable, font, radius, shadow or SPA icon.** Plugin elements live in the light DOM, put every CSS rule under their own tag, and read only the public `--c-*` variables. This follows the precedent of `golem15-journal-blogeditor` and `golem15-paymentgateway-quote-cart`. The one exception is the result-preview mimic (Color, exception E-1).
+
+Sources:
+- 32-CONTEXT.md D-01..D-25, D-90..D-97 (locked) and the Q-01 phase split. Phase 32 covers the seams, the plugin core, meta/canonical/robots, social overrides, redirects, normalisation, robots.txt, AI bots, verification, MCP and history.
+- ROADMAP Phase 32 success criteria 1–5.
+- The approved house conventions of the 10.1, 12.1, 16.3, 17 and 19.6.1 UI-SPECs.
+- The code scan: `admin/src/components/form/*` (FormView, FormField, FormTabs, WidgetField, PluginField, MLField, PermissionEditorField), `admin/src/app/icons.ts`, `modules/cabana/{form_schema,list_schema,schema_types}.go`, `modules/pact/capabilities.go` (`FormFieldType`, `AdminAction`, `AdminRecordAction` with `Prompt`, `AdminBulkAction`, `NavigationItem`, `SettingsItem`, `AdminSingleton`, `RowState`).
+- `sm-journal-plugin` (`models/post|category|tag/fields.yaml`, the `blogeditor`/`mlblogeditor` field-type pair, `assets/src/blogeditor/labels.ts`) and `sm-paymentgateway-plugin/assets/css/quote-cart.css`.
+- The PixelPixel `mpseo` pattern named in CONTEXT.
+
+---
+
+## Design System
+
+| Property | Value |
+|----------|-------|
+| Tool | none. There is no shadcn: the admin is a Vue 3 SPA with its own Direction C system. The shadcn gate does not apply, the same ruling as Phases 10.1, 12.1, 12.2, 16.3 and 19.6.1 |
+| Preset | not applicable |
+| Component library | Reka UI 2.9.10 (headless primitives), plus the local components in `admin/src/components/`. Plugin elements are framework-free TypeScript custom elements, built with Vite in `sm-seo-plugin/assets/src/` like the journal Blog Editor |
+| Icon library | SPA: `@lucide/vue` 1.17.0, through the `admin/src/app/icons.ts` registry names. Plugin elements: inline SVG copies of lucide 1.17.0 paths (`circle-check`, `triangle-alert`, `info`, `circle-alert`, `chevron-down`), 14px, `stroke="currentColor"`, `aria-hidden="true"`. Lucide is ISC-licensed, so the copies keep the licence line in the bundle header |
+| Font | DM Sans 400/600 (self-hosted `@fontsource/dm-sans`), plus DM Mono 400 for the robots.txt editor, the served-file view and URL values inside E6 and P5. The result-preview mimic uses `Arial, sans-serif` (exception E-1) |
+
+---
+
+## Component Inventory
+
+Enumerated by `node --input-type=module -e "import * as r from 'reka-ui'; console.log(Object.keys(r).length)"` (run in `admin/`) — 478 exports — reka-ui@2.9.10 — 2026-10-26.
+Local components enumerated by `find admin/src/components -name '*.vue' | wc -l` — 62 components — summercms-admin@unversioned (a private workspace package with no `version` field in `admin/package.json`, pinned by repo commit 28b6b55) — 2026-10-26.
+
+Icon presence was verified on 2026-10-26 by `node --input-type=module -e "import * as l from '@lucide/vue'; for (const n of ['Search','ArrowRight','TriangleAlert','Clock','Download','FileText','Link','CircleCheck','Info']) console.log(n, n in l)"` (run in `admin/`, @lucide/vue@1.17.0): every name is exported. The registry names `search`, `arrow-right`, `triangle-alert`, `clock`, `download`, `file-text` and `link` are already mapped in `admin/src/app/icons.ts`, so no registry change is needed.
+
+This table is a **non-exhaustive** list of known-good components for this phase, not a closed allowlist. Checking for another Reka export or local component is the expected path.
+
+| Component | Import path | Notes |
+|-----------|-------------|-------|
+| ListView | `admin/src/views/ListView.vue` | Redirects, 404 log, URL rules, History lists. No change |
+| FormView | `admin/src/views/FormView.vue` | Every form, including the extended content-type forms and the singleton screens. No change (C1) |
+| PreviewView | `admin/src/views/PreviewView.vue` | 404 log and History previews with record actions. No change |
+| SettingsIndexView / SettingsFormView | `admin/src/views/Settings*.vue` | The "SEO settings" tile and singleton form. No change |
+| FormTabs | `admin/src/components/form/FormTabs.vue` | Renders the SEO tab like any tab, including its error-count badge. No change |
+| FormField / FormGrid | `admin/src/components/form/` | Labels, comments, 422 lines and save notices (`summer-callout--warning` notice box) for every SEO field. No change |
+| WidgetField | `admin/src/components/form/fields/WidgetField.vue` | Hosts E3, E5 and E6 (`type: widget`). No change |
+| PluginField / MLPluginField / MLField | `admin/src/components/form/fields/` | Host E1, E2 and E4 (plugin field types). The ML variants put E1 and E2 under the shared locale switcher. No change |
+| PartialField / PartialHost | `admin/src/components/form/fields/PartialField.vue`, `admin/src/components/partial/PartialHost.vue` | P1–P5. No change |
+| MLTextField / MLTextareaField / TextField / TextareaField / DropdownField / SwitchField / CheckboxField / RepeaterField / FileuploadField / NumberField | `admin/src/components/form/fields/` | Every SEO field that needs no counter or directive editor. No change |
+| RecordActions + ConfirmDialog (prompt mode) | `admin/src/components/form/RecordActions.vue`, `admin/src/components/ui/ConfirmDialog.vue` | 404 log "Create redirect…" (prompted) and "Mark as gone", History "Undo". No change |
+| BulkActionsMenu, RowStateBadges, FilterBar, DataTable, CellValue | `admin/src/components/list/` | Bulk "Mark as gone", the "Chain", "Resolved" and "Undone" badges, scope/switch/daterange filters. No change |
+| ExtensionFailure | `admin/src/components/ui/ExtensionFailure.vue` | Shown when an SEO element's script fails or times out. No change |
+| `showToast` | `admin/src/state/useToasts.ts` | Action results and refusals. No change |
+| Partial kit | `admin/src/styles/main.css` (`.summer-callout*`, `.summer-table*`, `.summer-markdown`) | Used by P1–P5 and **inside** the plugin elements (they live in the light DOM, so the global kit classes apply). No change |
+
+**New plugin elements** (in `sm-seo-plugin`, one Vite bundle, every element defined by the same script):
+
+| ID | Tag | Registered as | Why no built-in type is enough |
+|----|-----|---------------|-------------------------------|
+| E1 | `golem15-seo-text` | Field types `mlseotext` (translatable) and `seotext` (single value), the same pair shape as `mlblogeditor`/`blogeditor`. Option `measure: title\|description` | `text`/`textarea` have no live character and pixel counter (D-09) |
+| E2 | `golem15-seo-robots` | Field types `mlseorobots` (translatable) and `seorobots` (single value). Option `scope: record\|rule\|type` | A robots value is two tri-state choices plus seven advanced directives in one stored string, per locale (D-08). `dropdown`/`checkbox` cannot be translatable, and nine separate fields could not follow the ML locale switcher |
+| E3 | `golem15-seo-serp` | `type: widget`, action `seo_snippet` | The live desktop/mobile result preview with pixel truncation, duplicate warnings and keyphrase checks (D-09) has no built-in equivalent |
+| E4 | `golem15-seo-robotstxt` | Field type `robotstxt` (single value) | A monospace editor with live line-numbered validation and a size meter (D-22). `textarea` validates nothing while typing |
+| E5 | `golem15-seo-robotstester` | `type: widget`, action `robots_test` | Tests unsaved robots.txt text and the AI bot switches against a URL and user agent (D-22) |
+| E6 | `golem15-seo-transfer` | `type: widget`, action `seo_transfer` | CSV export, then import with a dry-run preview and an all-or-nothing apply (D-12, D-13) |
+
+The bundle is listed in the `Scripts` of the `mlseotext` field type. Field-type scripts and styles load on every admin page, while controller `AdminClientAssets` load only on the plugin's own screens. E1–E3 therefore work on other plugins' forms, where the SEO tab is injected.
+
+**New server partials** (html/template in `sm-seo-plugin`, kit classes only, curated view models through `AdminPartialData`):
+
+| ID | Partial | Where |
+|----|---------|-------|
+| P1 | `seo_status` | Top of the SEO tab and of the URL rule form (update only): callouts |
+| P2 | `seo_effective` | End of the SEO tab and of the URL rule form (update only): the effective values with their source |
+| P3 | `placeholders` | SEO settings, General tab and each content-type tab: the placeholder table |
+| P4 | `robots_served` | robots.txt screen: the exact file served (saved version) |
+| P5 | `history_diff` | History preview: the field-by-field before/after table |
+
+---
+
+## Spacing Scale
+
+Declared values for new markup (all multiples of 4):
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| xs | 4px | Gap between a segmented control's segments (the PermissionEditor track `gap-1 p-1`); icon-to-text gap inside a 24px badge |
+| sm | 8px | Icon-to-text gap in status lines; gap between check lines in E3; gap between E5's input row items |
+| md | 16px | Result-card padding in E3; horizontal padding of E2 segments; column gap of E2's advanced grid and of E1's counter line; gap above E6's preview table |
+| lg | 24px | Gap between the two segmented controls in E2; gap between E6 sections |
+| xl | 32px | Not used by new markup (sticky footer padding, unchanged) |
+| 2xl | 48px | Not used |
+| 3xl | 64px | Not used |
+
+Exceptions (inherited from Direction C and approved in 16.3 and 19.6.1; no new value is introduced). The plugin elements copy some of these kit values on purpose, so they look identical to the native controls next to them: E4's editor and lint panel padding (12px × 14px, the table cell value), E6's actions row gap (10px, the footer button gap) and E2's 9px segment radius (a radius, copied from the PermissionEditor segments):
+- label-to-control gap 6px (`gap-1.5` in `FormField`);
+- form row gap 22px (`gap-y-[22px]`);
+- card padding 28px (`p-7`);
+- page stack 20px (`gap-5`);
+- footer button gap 10px (`gap-2.5`);
+- callout padding 14px × 18px (`.summer-callout`);
+- table cell padding 12px × 14px (`.summer-table td`);
+- control inner horizontal padding 14px (`px-3.5` in `controlClass`), reused by E1, E4 and every element input.
+
+E1's counter line sits 8px under its control, inside the element.
+
+Control heights reuse the Direction C set: inputs and selects 44px (`--spacing-input`), buttons 42px (`--spacing-button`), segments 34px (`--spacing-pager`), table head row 44px (`--spacing-row-head`).
+
+---
+
+## Typography
+
+Admin typography for new markup uses exactly three styles:
+
+| Role | Size | Weight | Line Height | Used by |
+|------|------|--------|-------------|---------|
+| Body | 14px | 400 | 1.5 | Field values, E5/E6 result text, table cells, callout text |
+| Label | 14px | 600 | 1.5 | Field labels (existing), E3/E6 section labels, checked segment text, button text, callout titles |
+| Meta | 13px | 400 | 1.5 | E1 counter line, E3 check lines, E4 lint lines and size meter, unchecked segment text, comments (existing) |
+
+Inherited, unchanged:
+- page heading 24px/700 (form) and 26px/700 (list) with `tracking-[-0.02em]`;
+- `.summer-table th` 12px/600 muted;
+- row-state badge 12px/600, also reused verbatim by E6's change badges (24px pill).
+
+This phase adds no admin size beyond 13px and 14px; with the inherited 12, 24 and 26px the admin keeps five sizes. Body (14/400) and Meta (13/400) are told apart by size, by Label's weight (600) and by Meta's muted colour, as in earlier phases. New content uses weights 400 and 600 only. Monospace: the E4 editor, the P4 served file, the E6 Address column and URL values in P5 use DM Mono 400 at 13px with line-height 1.6, the same values as `.summer-markdown pre`. This is the only DM Mono size.
+
+**E-1 result-preview mimic (content reproduction, not admin typography).** Inside E3's result card only, text reproduces Google's result rendering so that pixel truncation is truthful:
+- site name 14px/400 and breadcrumb URL 12px/400, `Arial, sans-serif`;
+- title 20px/400, line-height 1.3;
+- description 14px/400, line-height 1.58.
+
+These sizes exist solely to measure and show truncation. They never appear outside the result card, and they are not admin text styles.
+
+**Pixel measurement (E1 and E3 share one module).** Text width is measured with a canvas 2D context: `20px Arial` for titles and `14px Arial` for descriptions. The limits are 580px for titles (desktop and mobile), and 920px desktop / 680px mobile for descriptions. Truncation cuts at the last word boundary that fits, then appends ` …`. The thresholds are defaults (see Open questions Q-3).
+
+---
+
+## Color
+
+All admin colours are existing CSS variables (light / dark). New markup must not hard-code a hex value, apart from exception E-1.
+
+| Role | Value (light / dark) | Usage |
+|------|----------------------|-------|
+| Dominant (60%) | `bg` #f4f6f9 / #111726, `surface` #ffffff / #182033 | Pages, form and list cards, element inputs, E4 editor background |
+| Secondary (30%) | `subtle` #f3f5f8 / #1f283d, `border` #e6e9ef / #29334b, `border-strong` #d2d8e2 / #3a4661, `muted` #566175 / #a9b3c6 | Segmented tracks, E4 lint panel and served-file `pre`, counter text, the "Unchanged" badge, input borders, inactive segment text |
+| Accent (10%) | `primary` #22304d / #fcd34d; `sel` #fdf3cf / #3a3622; `ring` | See the reserved list |
+| Destructive | `danger` #c62828 / #f58a8a, `danger-soft` #fdf0f0 / #3a1d24 | Form-footer Delete, delete confirmations, 422 lines and banners, the "Noindex"/"Nofollow" checked segments, the "Refused" import badge, the "Chain" row badge (`negative`), refused-loop errors, E4 over-limit error |
+| Semantic OK | `ok-bg` #e3f4e8 / #173826, `ok-text` #1c6b35 / #8fdfa8 | "Good length" status icon and text, a passed keyphrase check, the "Index"/"Follow" checked segments, the "New" import badge, E5 "Allowed" |
+
+Accent reserved for (the list is exhaustive for this phase):
+1. **`primary` fill**:
+ - the one existing primary button per screen: footer **Save** on every form and singleton, and the list toolbar **create** on Redirects ("New redirect") and URL rules ("New URL rule");
+ - inside E6 only, the **"Apply :count changes"** button, the only primary inside the import card. The footer Save on that screen saves only the CSV options.
+2. **`primary` track/fill**: switches when on (AI bot switches, the per-type "Show the SEO tab" switch, "Lowercase addresses") and checked checkboxes, all existing controls.
+3. **`sel` tint**:
+ - `.summer-callout--warning` (staging lock, URL rule overrides, canonical warnings, chain and save notices);
+ - the "Changed" import badge;
+ - selected list rows (existing).
+4. **`ring` (3px)**: focus-visible on every focusable element, including every element control (each plugin element copies the base rule `outline: 3px solid var(--c-ring); outline-offset: 2px`, and inputs `box-shadow: 0 0 0 3px var(--c-ring)` plus a `primary` border).
+
+Accent is never used for: record-action buttons (outline), E3/E5/E6 secondary buttons (outline), counter text, check lines, segment tracks, links inside partials, or the result-preview card.
+
+Status is never shown by colour alone. Every status line pairs an icon with words, and every badge is text (WCAG 1.4.1).
+
+**E-1 result-preview mimic palette** (fixed in both admin themes, scoped to `golem15-seo-serp .serp-card` as element-local custom properties): card background `#ffffff`, site name and URL `#202124`, title `#1a0dab`, description `#4d5156`. The card has a 1px `var(--c-border)` border and a 12px radius. Rationale: the card reproduces a third-party page so that an editor recognises it. It is content, not admin chrome, so it stays light in dark mode, like a screenshot. No other element uses these values.
+
+---
+
+## Copywriting Contract
+
+All server copy lives in `sm-seo-plugin/lang/{en,pl}/lang.yaml` under `golem15.seo::lang.*`. Element copy (E1–E6) lives in each element's bundled `labels.ts` dictionary, chosen by the `ui-locale` attribute; this is the Blog Editor precedent. A widget's button caption comes from its action `Label`, as the 10.1 contract requires. Placeholders use `:name`. Plural texts are CLDR maps (`one`/`other` in en; `one`/`few`/`many`/`other` in pl), never pipe strings. Typographic quotes are “…” in en and „…” in pl. Existing framework copy (`form.*`, `list.*`, validation messages) is reused unchanged and not repeated here.
+
+### Primary CTAs and key states
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Redirects primary CTA (toolbar create; create-form title) | `redirects.new` | New redirect | Nowe przekierowanie |
+| URL rules primary CTA | `rules.new` | New URL rule | Nowa reguła adresu |
+| Import primary CTA (E6) | element `applyCount` | one: "Apply :count change" other: "Apply :count changes" | one: "Zastosuj :count zmianę" few: "Zastosuj :count zmiany" many: "Zastosuj :count zmian" other: "Zastosuj :count zmiany" |
+| Import preview (E6, action label) | `transfer.preview` | Preview import | Podgląd importu |
+| Export (E6) | element `download` | Download CSV | Pobierz CSV |
+| Robots tester (E5, action label) | `robots.test` | Test URL | Sprawdź adres |
+| History record action | `history.undo` | Undo this change | Cofnij tę zmianę |
+| Redirects empty state | `redirects.list_empty` | No redirects yet. Create one, import a CSV file, or create one from the 404 log. | Nie ma jeszcze przekierowań. Utwórz je, zaimportuj plik CSV lub utwórz je z dziennika 404. |
+| URL rules empty state | `rules.list_empty` | No URL rules yet. A rule sets SEO for an address that has no record behind it, such as the home page or a filter URL. | Nie ma jeszcze reguł adresów. Reguła ustawia SEO dla adresu, za którym nie stoi żaden rekord, np. strony głównej lub adresu z filtrem. |
+| 404 log empty state | `notfound.list_empty` | No 404 errors recorded. Addresses that return 404 appear here with their hit count. | Nie zarejestrowano błędów 404. Adresy zwracające 404 pojawią się tutaj z liczbą wejść. |
+| History empty state | `history.list_empty` | No SEO changes recorded yet. Every change from the admin, MCP or an import appears here and can be undone. | Nie zarejestrowano jeszcze zmian SEO. Każda zmiana z panelu, MCP lub importu pojawi się tutaj i będzie można ją cofnąć. |
+| Filtered empty (all lists) | existing `messages.list.empty_search` + hint | unchanged | unchanged |
+| List / form load error | existing `list.load_failed`, `form.load_failed` | unchanged | unchanged |
+| Element load error | existing `extension.widget_failed` | unchanged | unchanged |
+| Widget action error | existing `extension.action_failed` (or the server `error.message`) | unchanged | unchanged |
+
+### Navigation, settings tile, permission
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Main navigation item (icon `search`) | `nav.seo` | SEO | SEO |
+| Side menu: Redirects (icon `arrow-right`) | `nav.redirects` | Redirects | Przekierowania |
+| Side menu: 404 log (icon `triangle-alert`) | `nav.notfound` | 404 log | Dziennik 404 |
+| Side menu: URL rules (icon `link`) | `nav.rules` | URL rules | Reguły adresów |
+| Side menu: robots.txt (icon `file-text`) | `nav.robots` | robots.txt | robots.txt |
+| Side menu: History (icon `clock`) | `nav.history` | History | Historia zmian |
+| Side menu: Import and export (icon `download`) | `nav.transfer` | Import and export | Import i eksport |
+| Settings category | `settings.category` | SEO | SEO |
+| Settings tile label (icon `search`) | `settings.label` | SEO settings | Ustawienia SEO |
+| Settings tile description | `settings.description` | Title templates, default descriptions, indexing rules, address format and search engine verification. | Szablony tytułów, domyślne opisy, zasady indeksowania, format adresów i weryfikacja w wyszukiwarkach. |
+| Permission tab | `permissions.tab` | SEO | SEO |
+| Permission `golem15.seo.manage` (D-15) | `permissions.manage` | Manage SEO | Zarządzanie SEO |
+| Permission comment | `permissions.manage_comment` | Edit the SEO tab of every content type, URL rules, redirects, robots.txt and the SEO settings, in the admin and through MCP. | Edycja zakładki SEO we wszystkich typach treści, reguł adresów, przekierowań, robots.txt i ustawień SEO, w panelu i przez MCP. |
+
+### SEO tab (content-type forms and URL rule form)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Tab label | `tab.seo` | SEO | SEO |
+| Result preview label (E3 field label) | `field.snippet` | Search result preview | Podgląd w wynikach wyszukiwania |
+| SEO title label | `field.title` | SEO title | Tytuł SEO |
+| SEO title comment (content types) | `field.title_comment` | Leave empty to use the title template from the SEO settings. | Zostaw puste, aby użyć szablonu tytułu z ustawień SEO. |
+| SEO title comment (URL rules) | `field.title_comment_rule` | Leave empty to keep the title of the page or record at this address. | Zostaw puste, aby zachować tytuł strony lub rekordu pod tym adresem. |
+| Meta description label | `field.description` | Meta description | Opis meta |
+| Meta description comment | `field.description_comment` | Leave empty to use the description template, then the content's own excerpt. | Zostaw puste, aby użyć szablonu opisu, a potem zajawki treści. |
+| Focus keyphrase label | `field.keyphrase` | Focus keyphrase | Fraza kluczowa |
+| Focus keyphrase comment | `field.keyphrase_comment` | Checked in the preview above. Never shown on the site. | Sprawdzana w podglądzie powyżej. Nigdy nie jest wyświetlana na stronie. |
+| Canonical label | `field.canonical` | Canonical URL | Adres kanoniczny |
+| Canonical comment | `field.canonical_comment` | Leave empty for this page's own address. Accepts a path (/blog/post) or a full URL, also on another domain. | Zostaw puste dla własnego adresu strony. Przyjmuje ścieżkę (/blog/wpis) lub pełny adres, także w innej domenie. |
+| Robots label | `field.robots` | Search engine indexing | Indeksowanie w wyszukiwarkach |
+| Robots comment (record) | `field.robots_comment` | “Inherit” follows a URL rule for this address, then the content type default from the SEO settings. Also sent as an X-Robots-Tag header. | „Dziedzicz” stosuje regułę adresu, a potem domyślne ustawienie typu treści z ustawień SEO. Wysyłane także jako nagłówek X-Robots-Tag. |
+| Robots comment (URL rule) | `field.robots_comment_rule` | “Inherit” keeps the setting of the record at this address, then the content type default. | „Dziedzicz” zachowuje ustawienie rekordu pod tym adresem, a potem domyślne ustawienie typu treści. |
+| Social title label | `field.og_title` | Social title | Tytuł w mediach społecznościowych |
+| Social title comment | `field.og_title_comment` | Used by Facebook, LinkedIn and X. Empty fields use the SEO title, description and the content's image. | Używany przez Facebooka, LinkedIn i X. Puste pola przejmują tytuł SEO, opis i obraz treści. |
+| Social description label | `field.og_description` | Social description | Opis w mediach społecznościowych |
+| Social image label | `field.og_image` | Social image | Obraz w mediach społecznościowych |
+| Social image comment | `field.og_image_comment` | Recommended 1200 × 630 px. Shared by all languages of this record. | Zalecane 1200 × 630 px. Wspólny dla wszystkich wersji językowych tego rekordu. |
+| Effective values label (P2) | `field.effective` | What search engines get | Co otrzymują wyszukiwarki |
+
+### P1 `seo_status` and P2 `seo_effective` (server copy)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| URL rule override callout title (warning) | `status.rule_title` | A URL rule overrides part of this tab | Reguła adresu nadpisuje część tej zakładki |
+| URL rule override callout text | `status.rule_text` | The rule for “:url” sets :fields. Change them in SEO › URL rules. | Reguła dla „:url” ustawia: :fields. Zmień je w SEO › Reguły adresów. |
+| Canonical target noindex (warning, D-06) | `status.canonical_noindex` | The canonical URL “:url” is set to noindex, so search engines may ignore both pages. | Adres kanoniczny „:url” ma ustawione noindex, więc wyszukiwarki mogą zignorować obie strony. |
+| Canonical target redirected (warning, D-06) | `status.canonical_redirected` | The canonical URL “:url” redirects to “:target”. Point the canonical to the final address. | Adres kanoniczny „:url” przekierowuje na „:target”. Ustaw adres kanoniczny na adres docelowy. |
+| Staging lock row note | `status.staging` | On this environment every page is served with noindex and robots.txt blocks everything (non-production lock). | W tym środowisku każda strona ma noindex, a robots.txt blokuje wszystko (blokada środowiska nieprodukcyjnego). |
+| P2 column: property / value / source | `effective.property` / `effective.value` / `effective.source` | Property / Value / Source | Właściwość / Wartość / Źródło |
+| P2 locale heading | `effective.locale` | “:locale” — :url | „:locale” — :url |
+| P2 sources | `effective.source_rule` / `_record` / `_template` / `_global` / `_content` / `_default` | URL rule / This record / Template / SEO settings / Content / Site default | Reguła adresu / Ten rekord / Szablon / Ustawienia SEO / Treść / Domyślne dla witryny |
+| P2 empty value | existing `list.empty_value` | — | — |
+| P2 no address yet | `effective.no_url` | This language has no address yet. Fill in its slug and save. | Ta wersja językowa nie ma jeszcze adresu. Uzupełnij jej slug i zapisz. |
+
+### E1 `golem15-seo-text` (element copy)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Counter | `count` | :chars characters · :px / :max px | Znaki: :chars · :px / :max px |
+| Status: good | `good` | Good length | Dobra długość |
+| Status: empty, title | `emptyTitle` | Empty: the title template is used | Puste: używany jest szablon tytułu |
+| Status: empty, description | `emptyDescription` | Empty: the template or the excerpt is used | Puste: używany jest szablon lub zajawka |
+| Status: short, title (< 30 characters) | `shortTitle` | Short: aim for 30–60 characters | Krótki: celuj w 30–60 znaków |
+| Status: short, description (< 120 characters) | `shortDescription` | Short: aim for 120–160 characters | Krótki: celuj w 120–160 znaków |
+| Status: long, title (> 580 px) | `longTitle` | Too long: Google cuts titles after about 580 px | Za długi: Google ucina tytuły po ok. 580 px |
+| Status: long, description (> 920 px) | `longDescription` | Too long: Google cuts descriptions after about 920 px on desktop and 680 px on mobile | Za długi: Google ucina opisy po ok. 920 px na komputerze i 680 px na telefonie |
+
+### E2 `golem15-seo-robots` (element copy)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Group: indexing | `indexing` | Indexing | Indeksowanie |
+| Group: links | `links` | Links | Linki |
+| Segment: inherit (`scope: record`/`rule`) | `inherit` | Inherit | Dziedzicz |
+| Segment: inherit (`scope: type`) | `siteDefault` | Site default | Domyślne |
+| Segments: index / noindex | `index` / `noindex` | Index / Noindex | Indeksuj / Nie indeksuj |
+| Segments: follow / nofollow | `follow` / `nofollow` | Follow / Nofollow | Śledź / Nie śledź |
+| Advanced disclosure | `advanced` | Advanced directives | Dyrektywy zaawansowane |
+| Advanced summary when set | `advancedSet` | one: ":count set" other: ":count set" | one: "ustawiona :count" few: "ustawione :count" many: "ustawionych :count" other: "ustawione :count" |
+| max-snippet | `maxSnippet` | Maximum snippet length (characters) | Maksymalna długość fragmentu (znaki) |
+| max-snippet hint | `maxSnippetHint` | Empty: no limit. 0: no snippet. -1: no limit. | Puste: bez limitu. 0: bez fragmentu. -1: bez limitu. |
+| max-image-preview | `maxImagePreview` | Image preview size | Rozmiar podglądu obrazu |
+| max-image-preview options | `notSet` / `none` / `standard` / `large` | Not set / None / Standard / Large | Nie ustawiono / Brak / Standardowy / Duży |
+| max-video-preview | `maxVideoPreview` | Maximum video preview (seconds) | Maksymalny podgląd wideo (sekundy) |
+| Checkboxes | `noarchive` / `nosnippet` / `noimageindex` | Do not show a cached copy (noarchive) / Do not show a snippet (nosnippet) / Do not index images (noimageindex) | Nie pokazuj kopii (noarchive) / Nie pokazuj fragmentu (nosnippet) / Nie indeksuj obrazów (noimageindex) |
+| unavailable_after | `unavailableAfter` | Remove from results after | Usuń z wyników po dniu |
+| Invalid number | `invalidNumber` | Enter a whole number, -1 or more. | Wpisz liczbę całkowitą, -1 lub większą. |
+
+### E3 `golem15-seo-serp` (element copy)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Device segments | `desktop` / `mobile` | Desktop / Mobile | Komputer / Telefon |
+| Previewed language | `language` | Language: :locale | Język: :locale |
+| Title from template | `fromTemplate` | Title from the template “:template” | Tytuł z szablonu „:template” |
+| Description from content | `fromContent` | Description from the content excerpt | Opis z zajawki treści |
+| Description missing | `noDescription` | No description: Google picks text from the page. | Brak opisu: Google wybierze tekst ze strony. |
+| Address not yet known | `noUrl` | The address appears after the first save. | Adres pojawi się po pierwszym zapisie. |
+| Saved data note | `savedNote` | The address and the H1 come from the saved record. Save to refresh them. | Adres i H1 pochodzą z zapisanego rekordu. Zapisz, aby je odświeżyć. |
+| Checking duplicates | `checking` | Checking for duplicates… | Sprawdzanie duplikatów… |
+| Duplicate title | `dupTitle` | one: "Same title as :count other page: :list" other: "Same title as :count other pages: :list" | one: "Ten sam tytuł co :count inna strona: :list" few: "Ten sam tytuł co :count inne strony: :list" many: "Ten sam tytuł co :count innych stron: :list" other: "Ten sam tytuł co :count innej strony: :list" |
+| Duplicate description | `dupDescription` | one: "Same description as :count other page: :list" other: "Same description as :count other pages: :list" | one: "Ten sam opis co :count inna strona: :list" few: "Ten sam opis co :count inne strony: :list" many: "Ten sam opis co :count innych stron: :list" other: "Ten sam opis co :count innej strony: :list" |
+| More in list | `andMore` | and :count more | i :count więcej |
+| Keyphrase heading | `keyphrase` | Focus keyphrase “:phrase” | Fraza kluczowa „:phrase” |
+| Check: title | `kpTitle` / `kpTitleNo` | In the SEO title / Not in the SEO title | W tytule SEO / Brak w tytule SEO |
+| Check: H1 | `kpH1` / `kpH1No` | In the H1 / Not in the H1 | W nagłówku H1 / Brak w nagłówku H1 |
+| Check: description | `kpDesc` / `kpDescNo` | In the meta description / Not in the meta description | W opisie meta / Brak w opisie meta |
+| Check: URL | `kpUrl` / `kpUrlNo` | In the address / Not in the address | W adresie / Brak w adresie |
+| Check: text length (≥ 300 words) | `kpLength` / `kpLengthNo` | Text length: :count words / Text length: :count words, aim for at least 300 | Długość tekstu: :count słów / Długość tekstu: :count słów, celuj w co najmniej 300 |
+| Widget action label | `snippet.action` | Check duplicates | Sprawdź duplikaty |
+
+### SEO settings (singleton settings form)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Tabs | `settings.tab_general` / `_indexing` / `_urls` / `_verification` | General / Indexing / Addresses / Verification | Ogólne / Indeksowanie / Adresy / Weryfikacja |
+| Content-type tabs | the type's signpost label | (e.g. Posts, Categories, Tags) | (e.g. Wpisy, Kategorie, Tagi) |
+| Site name | `settings.site_name` | Site name | Nazwa witryny |
+| Site name comment | `settings.site_name_comment` | Replaces %site% in title templates. | Zastępuje %site% w szablonach tytułów. |
+| Title template | `settings.title_template` | Title template | Szablon tytułu |
+| Title template comment | `settings.title_template_comment` | Used when a page has no SEO title. Default: %title% \| %site%. | Używany, gdy strona nie ma tytułu SEO. Domyślnie: %title% \| %site%. |
+| Home page template | `settings.home_template` | Home page title template | Szablon tytułu strony głównej |
+| Default description | `settings.default_description` | Default meta description | Domyślny opis meta |
+| Default description comment | `settings.default_description_comment` | Used when neither the page, its template nor its content provides a description. | Używany, gdy ani strona, ani jej szablon, ani treść nie dostarczają opisu. |
+| Default social image | `settings.default_og_image` | Default social image | Domyślny obraz w mediach społecznościowych |
+| X account | `settings.twitter_site` | X (Twitter) account | Konto X (Twitter) |
+| X account comment | `settings.twitter_site_comment` | Your handle with @, e.g. @summercms. Sent as twitter:site. | Nazwa konta z @, np. @summercms. Wysyłana jako twitter:site. |
+| Placeholder table (P3) label / columns | `settings.placeholders` / `_code` / `_meaning` | Placeholders / Placeholder / Replaced with | Znaczniki / Znacznik / Zastępowany przez |
+| P3 rows | `placeholder.title` / `.description` / `.site` | The content's title / The content's own description or excerpt / The site name | Tytuł treści / Własny opis lub zajawka treści / Nazwa witryny |
+| Per-type: show tab | `settings.type_inject` | Show the SEO tab on :type | Pokazuj zakładkę SEO w: :type |
+| Per-type: show tab comment | `settings.type_inject_comment` | Off hides the tab. Saved values are kept and still used. | Wyłączenie ukrywa zakładkę. Zapisane wartości zostają i nadal działają. |
+| Per-type: title template | `settings.type_title_template` | Title template for :type | Szablon tytułu dla: :type |
+| Per-type: title template comment | `settings.type_title_template_comment` | Leave empty to use the general title template. | Zostaw puste, aby użyć ogólnego szablonu tytułu. |
+| Per-type: description template | `settings.type_description_template` | Description template for :type | Szablon opisu dla: :type |
+| Per-type: default robots | `settings.type_robots` | Default indexing for :type | Domyślne indeksowanie dla: :type |
+| Query parameters (repeater) | `settings.query_params` | Query parameters | Parametry adresu |
+| Query parameters comment | `settings.query_params_comment` | Decides which parameters stay in the canonical URL. A trailing * matches a prefix, e.g. utm_*. | Decyduje, które parametry zostają w adresie kanonicznym. Gwiazdka na końcu pasuje do prefiksu, np. utm_*. |
+| Repeater row: parameter / kind / path | `settings.param_name` / `_kind` / `_path` | Parameter / Kind / Only under path | Parametr / Rodzaj / Tylko pod ścieżką |
+| Kind options | `settings.param_content` / `_filter` / `_tracking` | Content: stays in the canonical / Filter or sort: dropped, page gets noindex, follow / Tracking: dropped | Treść: zostaje w adresie kanonicznym / Filtr lub sortowanie: usuwany, strona dostaje noindex, follow / Śledzenie: usuwany |
+| Path comment | `settings.param_path_comment` | Empty: the whole site. Example: /katalog/* | Puste: cała witryna. Przykład: /katalog/* |
+| Repeater add | `settings.param_add` | Add parameter | Dodaj parametr |
+| noindex paths | `settings.noindex_paths` | Addresses never indexed | Adresy nigdy nieindeksowane |
+| noindex paths comment | `settings.noindex_paths_comment` | One address or pattern per line, e.g. /konto/* or /logowanie. These pages get noindex, nofollow. Pages behind sign-in are marked automatically. | Jeden adres lub wzorzec w wierszu, np. /konto/* lub /logowanie. Te strony dostają noindex, nofollow. Strony wymagające logowania są oznaczane automatycznie. |
+| Search paths | `settings.search_paths` | Internal search results | Wyniki wyszukiwania wewnętrznego |
+| Search paths comment | `settings.search_paths_comment` | One address or pattern per line, e.g. /szukaj. These pages get noindex, follow. | Jeden adres lub wzorzec w wierszu, np. /szukaj. Te strony dostają noindex, follow. |
+| Host form | `settings.url_host` | Domain form | Forma domeny |
+| Host options | `settings.url_host_keep` / `_www` / `_bare` | Do not change / Always with www / Always without www | Nie zmieniaj / Zawsze z www / Zawsze bez www |
+| Trailing slash | `settings.url_slash` | Trailing slash | Ukośnik na końcu |
+| Trailing slash options | `settings.url_slash_keep` / `_add` / `_remove` | Do not change / Always add (/blog/) / Always remove (/blog) | Nie zmieniaj / Zawsze dodawaj (/blog/) / Zawsze usuwaj (/blog) |
+| Lowercase | `settings.url_lowercase` | Lowercase addresses | Małe litery w adresach |
+| Address comment (on host, slash, lowercase) | `settings.url_change_comment` | A change sends every non-matching address to the new form with a 301. Check the site's current addresses first. | Zmiana przekierowuje (301) każdy niepasujący adres na nową formę. Najpierw sprawdź obecne adresy witryny. |
+| Fixed rules note (comment on Domain form) | `settings.url_fixed_comment` | Always on: HTTPS, removal of double slashes and /index.html, a single 301 hop to the final address, and strict addresses (every segment must match the record). | Zawsze włączone: HTTPS, usuwanie podwójnych ukośników i /index.html, jedno przekierowanie 301 do adresu docelowego oraz ścisłe adresy (każdy segment musi pasować do rekordu). |
+| Google / Bing / Yandex codes | `settings.verify_google` / `_bing` / `_yandex` | Google verification code / Bing verification code / Yandex verification code | Kod weryfikacyjny Google / Kod weryfikacyjny Bing / Kod weryfikacyjny Yandex |
+| Code comment | `settings.verify_code_comment` | Paste only the content value of the meta tag. It is added to the home page. | Wklej tylko wartość content znacznika meta. Zostanie dodany na stronie głównej. |
+| Other meta tags (repeater) | `settings.verify_other` / `_name` / `_content` / `_add` | Other verification meta tags / Meta name / Content / Add meta tag | Inne znaczniki weryfikacyjne / Nazwa meta / Wartość / Dodaj znacznik |
+| Verification files (repeater) | `settings.verify_files` / `_filename` / `_body` / `_add` | Verification files / File name / File content / Add file | Pliki weryfikacyjne / Nazwa pliku / Zawartość pliku / Dodaj plik |
+| Files comment | `settings.verify_files_comment` | Served from the site root, e.g. /google1234.html. | Serwowane z katalogu głównego witryny, np. /google1234.html. |
+| Invalid file name (422) | `settings.verify_filename_invalid` | Use a plain file name ending in .html, .txt or .xml, without slashes, e.g. google1234.html. | Użyj samej nazwy pliku z końcówką .html, .txt lub .xml, bez ukośników, np. google1234.html. |
+| Duplicate file name (422) | `settings.verify_filename_taken` | Another verification file already uses this name. | Ten plik weryfikacyjny już istnieje. |
+| Template has unknown placeholder (422) | `settings.template_unknown` | Unknown placeholder “:placeholder”. Use one from the table below the templates. | Nieznany znacznik „:placeholder”. Użyj jednego z tabeli pod szablonami. |
+
+### URL rules
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| List title / record name | `rules.list_title` / `rules.name` | URL rules / URL rule | Reguły adresów / Reguła adresu |
+| Address | `rules.url` | Address | Adres |
+| Match | `rules.match` | Match | Dopasowanie |
+| Match options | `rules.match_exact` / `rules.match_pattern` | Exact address / Pattern | Dokładny adres / Wzorzec |
+| Note | `rules.note` | Note | Notatka |
+| Note comment | `rules.note_comment` | Why this rule exists, e.g. the audit it comes from. Not shown on the site. | Dlaczego ta reguła istnieje, np. z jakiego audytu pochodzi. Nie jest wyświetlana na stronie. |
+| Columns | `rules.col_overrides` / `rules.col_updated` | Sets / Updated | Ustawia / Zaktualizowano |
+| Address comment, full (used on the form; precedence sentence first) | `rules.url_comment_full` | A URL rule wins over the record's SEO tab and the SEO settings. A path such as / or /blog?tag=*. In a pattern, * matches any text. | Reguła adresu ma pierwszeństwo przed zakładką SEO rekordu i ustawieniami SEO. Ścieżka, np. / lub /blog?tag=*. We wzorcu * pasuje do dowolnego tekstu. |
+| Duplicate address (422) | `rules.url_taken` | A rule for this address and match already exists. | Reguła dla tego adresu i dopasowania już istnieje. |
+| Invalid address (422) | `rules.url_invalid` | Start the address with /. Use * only in a pattern. | Zacznij adres od /. Używaj * tylko we wzorcu. |
+| Delete confirm | `rules.delete_confirm` | Delete this URL rule? The address falls back to its record and the SEO settings. You can undo it in History. | Usunąć tę regułę adresu? Adres wróci do ustawień rekordu i ustawień SEO. Możesz to cofnąć w Historii zmian. |
+| Delete selected confirm | `rules.delete_selected_confirm` | one: "Delete :count URL rule? You can undo it in History." other: "Delete :count URL rules? You can undo it in History." | one: "Usunąć :count regułę adresu? Możesz to cofnąć w Historii zmian." few: "Usunąć :count reguły adresów? Możesz to cofnąć w Historii zmian." many: "Usunąć :count reguł adresów? Możesz to cofnąć w Historii zmian." other: "Usunąć :count reguły adresu? Możesz to cofnąć w Historii zmian." |
+
+### Redirects
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| List title / record name | `redirects.list_title` / `redirects.name` | Redirects / Redirect | Przekierowania / Przekierowanie |
+| Source | `redirects.source` | From | Z adresu |
+| Source comment | `redirects.source_comment` | Exact: /old-post. Pattern: /old/* (the matched part replaces * in the target). Regex: ^/p/(\d+)$ (use $1 in the target). | Dokładny: /stary-wpis. Wzorzec: /stare/* (dopasowana część zastępuje * w celu). Regex: ^/p/(\d+)$ (użyj $1 w celu). |
+| Match | `redirects.match` | Match | Dopasowanie |
+| Match options | `redirects.match_exact` / `_pattern` / `_regex` | Exact / Pattern / Regex | Dokładne / Wzorzec / Regex |
+| Target | `redirects.target` | To | Na adres |
+| Target comment | `redirects.target_comment` | A path or a full URL. Leave empty for 410 Gone. | Ścieżka lub pełny adres. Zostaw puste dla 410 Gone. |
+| Status | `redirects.status` | Status | Kod |
+| Status options | `redirects.status_301` / `_302` / `_410` | 301 Moved permanently / 302 Found (temporary) / 410 Gone | 301 Przeniesiono na stałe / 302 Przeniesiono tymczasowo / 410 Usunięto |
+| Query string | `redirects.query` | Query string | Parametry adresu |
+| Query options | `redirects.query_ignore` / `_pass` | Ignore it / Pass it to the target | Ignoruj / Przekaż do celu |
+| Ignore case | `redirects.ignore_case` | Ignore letter case | Ignoruj wielkość liter |
+| Trailing slash | `redirects.trailing_slash` | Match with and without a trailing slash | Dopasuj z ukośnikiem na końcu i bez |
+| Note | `redirects.note` | Note | Notatka |
+| Column: origin | `redirects.origin` | Origin | Pochodzenie |
+| Origin values | `redirects.origin_manual` / `_slug` / `_import` / `_mcp` / `_notfound` | Manual / Address change / Import / MCP / 404 log | Ręcznie / Zmiana adresu / Import / MCP / Dziennik 404 |
+| Column: updated | `redirects.updated` | Updated | Zaktualizowano |
+| Row state `negative` | `redirects.state_chain` | Chain | Łańcuch |
+| Filter: chains (scope) | `redirects.filter_chain` / `_only` / `_hide` | Chains / Only chains / Hide chains | Łańcuchy / Tylko łańcuchy / Ukryj łańcuchy |
+| Loop refused (422 on target, D-19) | `redirects.loop` | This redirect would create a loop: :path. Nothing was saved. | To przekierowanie utworzyłoby pętlę: :path. Nic nie zapisano. |
+| Chain notice (save notice, D-19) | `redirects.chain_notice` | Saved, but this redirect is part of a chain: :path. Visitors take :count hops. Point “:source” straight to “:final”. | Zapisano, ale to przekierowanie jest częścią łańcucha: :path. Odwiedzający przechodzą :count przekierowania. Ustaw „:source” bezpośrednio na „:final”. |
+| Target missing for 301/302 (422) | `redirects.target_required` | Enter the address to redirect to, or choose 410 Gone. | Wpisz adres docelowy lub wybierz 410 Usunięto. |
+| Target set for 410 (422) | `redirects.target_gone` | A 410 Gone rule sends visitors nowhere. Clear the target, or choose 301 or 302. | Reguła 410 Usunięto nikogo nie przekierowuje. Wyczyść adres docelowy lub wybierz 301 albo 302. |
+| Invalid regex (422) | `redirects.regex_invalid` | This is not a valid regular expression: :error | To nie jest poprawne wyrażenie regularne: :error |
+| Invalid source (422) | `redirects.source_invalid` | Start the address with /. Use * only in a pattern. | Zacznij adres od /. Używaj * tylko we wzorcu. |
+| Duplicate source (422) | `redirects.source_taken` | Another redirect already uses this address and match. | Inne przekierowanie już używa tego adresu i dopasowania. |
+| Automatic redirect notice (record save notice on the owner's slug field, D-18) | `redirects.auto_created` | The address changed, so a 301 from “:old” to “:new” was created. :count earlier redirects now point straight to the new address. | Adres się zmienił, więc utworzono przekierowanie 301 z „:old” na „:new”. Wcześniejsze przekierowania (:count) prowadzą teraz bezpośrednio na nowy adres. |
+| Delete confirm | `redirects.delete_confirm` | Delete this redirect? Its source address will return 404 again. You can undo it in History. | Usunąć to przekierowanie? Jego adres źródłowy znowu zwróci 404. Możesz to cofnąć w Historii zmian. |
+| Delete selected confirm | `redirects.delete_selected_confirm` | one: "Delete :count redirect? Its source address will return 404 again. You can undo it in History." other: "Delete :count redirects? Their source addresses will return 404 again. You can undo it in History." | one: "Usunąć :count przekierowanie? Jego adres źródłowy znowu zwróci 404. Możesz to cofnąć w Historii zmian." few: "Usunąć :count przekierowania? Ich adresy źródłowe znowu zwrócą 404. Możesz to cofnąć w Historii zmian." many: "Usunąć :count przekierowań? Ich adresy źródłowe znowu zwrócą 404. Możesz to cofnąć w Historii zmian." other: "Usunąć :count przekierowania? Ich adresy źródłowe znowu zwrócą 404. Możesz to cofnąć w Historii zmian." |
+
+### 404 log
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| List title / record name | `notfound.list_title` / `notfound.name` | 404 log / 404 address | Dziennik 404 / Adres 404 |
+| Columns | `notfound.url` / `_hits` / `_last_hit` / `_first_seen` / `_referer` | Address / Hits / Last hit / First seen / Last referer | Adres / Wejścia / Ostatnie wejście / Pierwsze wejście / Ostatnie źródło |
+| No referer | existing `list.empty_value` | — | — |
+| Row state `disabled` | `notfound.state_resolved` | Resolved | Rozwiązany |
+| Filter: status (scope) | `notfound.filter_status` / `_open` / `_resolved` | Status / Open / Resolved | Stan / Otwarte / Rozwiązane |
+| Preview subtitle | `notfound.preview` | Address that returned 404 | Adres, który zwrócił 404 |
+| Resolved hint (preview header partial, plain `.summer-callout`) | `notfound.resolved_hint` | Resolved by “:rule”. This address no longer returns 404. | Rozwiązane przez „:rule”. Ten adres nie zwraca już 404. |
+| Record action: redirect (prompted) | `notfound.redirect` | Create redirect… | Utwórz przekierowanie… |
+| Prompt label / placeholder / comment | `notfound.redirect_prompt` / `_placeholder` / `_comment` | Redirect to (301) / /new-address / A path or a full URL. To use 302, edit the redirect afterwards. | Przekieruj na (301) / /nowy-adres / Ścieżka lub pełny adres. Aby użyć 302, edytuj potem przekierowanie. |
+| Redirect confirm | `notfound.redirect_confirm` | Create a 301 redirect from “:url”? | Utworzyć przekierowanie 301 z „:url”? |
+| Redirect success | `notfound.redirected` | Redirect “:url” → “:target” created. | Utworzono przekierowanie „:url” → „:target”. |
+| Record + bulk action: gone | `notfound.gone` | Mark as gone (410) | Oznacz jako usunięty (410) |
+| Gone confirm (record) | `notfound.gone_confirm` | Answer “:url” with 410 Gone? Search engines drop it faster than a 404. You can undo it in History. | Odpowiadać na „:url” kodem 410 Gone? Wyszukiwarki usuną go szybciej niż przy 404. Możesz to cofnąć w Historii zmian. |
+| Gone confirm (bulk) | `notfound.gone_selected_confirm` | one: "Answer :count address with 410 Gone? You can undo it in History." other: "Answer :count addresses with 410 Gone? You can undo it in History." | one: "Odpowiadać kodem 410 Gone na :count adres? Możesz to cofnąć w Historii zmian." few: "Odpowiadać kodem 410 Gone na :count adresy? Możesz to cofnąć w Historii zmian." many: "Odpowiadać kodem 410 Gone na :count adresów? Możesz to cofnąć w Historii zmian." other: "Odpowiadać kodem 410 Gone na :count adresu? Możesz to cofnąć w Historii zmian." |
+| Gone success (record / bulk) | `notfound.gone_done` / `notfound.gone_selected_done` | 410 Gone created for “:url”. / 410 Gone created for the selected addresses. | Utworzono 410 Gone dla „:url”. / Utworzono 410 Gone dla zaznaczonych adresów. |
+| Redirect would loop (403 Message, toast) | `redirects.loop` | (as Redirects) | (as Redirects) |
+| Address already has a rule (403 Message, toast) | `notfound.has_rule` | A redirect for “:url” already exists. Edit it in Redirects. | Przekierowanie dla „:url” już istnieje. Edytuj je w Przekierowaniach. |
+| Delete selected confirm | `notfound.delete_selected_confirm` | one: "Remove :count address from the log? It is added again on its next 404." other: "Remove :count addresses from the log? They are added again on their next 404." | one: "Usunąć :count adres z dziennika? Wróci przy następnym błędzie 404." few: "Usunąć :count adresy z dziennika? Wrócą przy następnym błędzie 404." many: "Usunąć :count adresów z dziennika? Wrócą przy następnym błędzie 404." other: "Usunąć :count adresu z dziennika? Wrócą przy następnym błędzie 404." |
+
+### robots.txt screen (singleton) and AI bots
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Screen title | `robots.title` | robots.txt | robots.txt |
+| Tabs | `robots.tab_file` / `robots.tab_bots` | robots.txt / AI bots | robots.txt / Boty AI |
+| Staging callout title / text (warning, on top of tab 1) | `robots.staging_title` / `robots.staging_text` | This is not the production environment / Crawlers get “Disallow: /” and every page is sent with “X-Robots-Tag: noindex”, whatever you save here. The text below applies in production. | To nie jest środowisko produkcyjne / Roboty dostają „Disallow: /”, a każda strona jest wysyłana z „X-Robots-Tag: noindex”, niezależnie od tego, co tu zapiszesz. Poniższy tekst obowiązuje na produkcji. |
+| Editor label | `robots.content` | robots.txt rules | Reguły robots.txt |
+| Editor comment | `robots.content_comment` | Sitemap lines and the AI bot rules are added automatically when the file is served. | Wiersze Sitemap i reguły botów AI są dodawane automatycznie przy serwowaniu pliku. |
+| Tester label (E5 field label) | `robots.tester` | Test an address | Sprawdź adres |
+| Served file label (P4) | `robots.served` | What crawlers get | Co otrzymują roboty |
+| Served file comment | `robots.served_comment` | The saved version, as served at /robots.txt. Save to update it. | Zapisana wersja, tak jak jest serwowana pod /robots.txt. Zapisz, aby ją odświeżyć. |
+| Over the size limit (422 from Decode) | `robots.too_large` | robots.txt may be at most 500 KB. Google ignores everything after that. | robots.txt może mieć najwyżej 500 KB. Google ignoruje wszystko powyżej. |
+| Blocked-noindex notice (save notice, D-24) | `robots.blocks_noindex` | one: ":count noindex address is blocked by these rules, so Google cannot see its noindex: :list" other: ":count noindex addresses are blocked by these rules, so Google cannot see their noindex: :list" | one: ":count adres z noindex jest blokowany przez te reguły, więc Google nie zobaczy noindex: :list" few: ":count adresy z noindex są blokowane przez te reguły, więc Google nie zobaczy noindex: :list" many: ":count adresów z noindex jest blokowanych przez te reguły, więc Google nie zobaczy noindex: :list" other: ":count adresu z noindex jest blokowane przez te reguły, więc Google nie zobaczy noindex: :list" |
+| AI bot switch label | the bot's user-agent token | GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, Google-Extended, PerplexityBot, CCBot, Applebot-Extended, Meta-ExternalAgent | (same tokens) |
+| AI search bot comment | `bots.search_comment` | :operator · AI search and answers. Off blocks it in robots.txt. | :operator · wyszukiwanie i odpowiedzi AI. Wyłączenie blokuje go w robots.txt. |
+| AI training bot comment | `bots.training_comment` | :operator · collects content for AI training. Off blocks it in robots.txt. | :operator · zbiera treści do trenowania AI. Wyłączenie blokuje go w robots.txt. |
+| User-fetch bot comment | `bots.user_comment` | :operator · fetches a page when a user asks for it. Off blocks it in robots.txt. | :operator · pobiera stronę na prośbę użytkownika. Wyłączenie blokuje go w robots.txt. |
+
+### E4 `golem15-seo-robotstxt` (element copy, live lint)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Size meter | `size` | :size of 500 KB | :size z 500 KB |
+| No findings | `clean` | No problems found. | Nie znaleziono problemów. |
+| Unknown directive | `unknown` | Line :line: “:word” is not a robots.txt directive. | Wiersz :line: „:word” nie jest dyrektywą robots.txt. |
+| Unknown with suggestion | `unknownSuggest` | Line :line: “:word” is not a robots.txt directive. Did you mean “:suggestion”? | Wiersz :line: „:word” nie jest dyrektywą robots.txt. Czy chodziło o „:suggestion”? |
+| Rule before any User-agent | `noGroup` | Line :line: this rule has no User-agent line above it, so crawlers ignore it. | Wiersz :line: nad tą regułą nie ma wiersza User-agent, więc roboty ją ignorują. |
+| Bad path | `badPath` | Line :line: a path must start with / or *. | Wiersz :line: ścieżka musi zaczynać się od / lub *. |
+| Missing colon | `noColon` | Line :line: write the directive as “Name: value”. | Wiersz :line: zapisz dyrektywę jako „Nazwa: wartość”. |
+| Own Sitemap line | `ownSitemap` | Line :line: Sitemap lines are added automatically. This one is kept as written. | Wiersz :line: wiersze Sitemap są dodawane automatycznie. Ten zostaje bez zmian. |
+| Crawl-delay | `crawlDelay` | Line :line: Google ignores Crawl-delay. Bing and Yandex use it. | Wiersz :line: Google ignoruje Crawl-delay. Bing i Yandex go stosują. |
+| More findings | `moreFindings` | and :count more | i :count więcej |
+| Over the limit | `tooLarge` | Over 500 KB: the file cannot be saved. | Ponad 500 KB: nie można zapisać pliku. |
+
+### E5 `golem15-seo-robotstester` (element copy)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Address input label / placeholder | `url` / `urlPlaceholder` | Address / /konto/ustawienia | Adres / /konto/ustawienia |
+| User agent label | `agent` | Crawler | Robot |
+| Allowed by rule | `allowedRule` | Allowed for :agent. Rule: line :line “:rule”. | Dozwolone dla :agent. Reguła: wiersz :line „:rule”. |
+| Allowed, no rule | `allowedNone` | Allowed for :agent: no rule matches this address. | Dozwolone dla :agent: żadna reguła nie pasuje do tego adresu. |
+| Blocked | `blocked` | Blocked for :agent by line :line “:rule”. | Zablokowane dla :agent przez wiersz :line „:rule”. |
+| Blocked by AI switch | `blockedBot` | Blocked for :agent by its switch on the AI bots tab. | Zablokowane dla :agent przełącznikiem w zakładce Boty AI. |
+| Unsaved note | `unsaved` | Tests the text in the editor and the AI bot switches, including unsaved changes. | Sprawdza tekst w edytorze i przełączniki botów AI, także niezapisane zmiany. |
+| Staging note | `stagingNote` | On this environment every address is blocked. The result shows the production behaviour. | W tym środowisku każdy adres jest zablokowany. Wynik pokazuje zachowanie na produkcji. |
+| Empty address | `urlRequired` | Enter an address starting with /. | Wpisz adres zaczynający się od /. |
+
+### History
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| List title / record name | `history.list_title` / `history.name` | History / Change | Historia zmian / Zmiana |
+| Columns | `history.when` / `_who` / `_via` / `_what` / `_change` / `_batch` | When / Who / Via / What / Change / Batch | Kiedy / Kto / Skąd / Czego dotyczy / Zmiana / Partia |
+| Via values | `history.via_admin` / `_mcp` / `_import` / `_auto` / `_undo` | Admin / MCP / Import / Automatic / Undo | Panel / MCP / Import / Automatycznie / Cofnięcie |
+| What values (examples, server-built) | `history.what_record` / `_rule` / `_redirect` / `_robots` / `_settings` | SEO of :type “:title” (:locale) / URL rule “:url” / Redirect “:url” / robots.txt / SEO settings | SEO: :type „:title” (:locale) / Reguła adresu „:url” / Przekierowanie „:url” / robots.txt / Ustawienia SEO |
+| Change cell | `history.change_cell` | one: ":field: :before → :after" other: ":count fields changed" | one: ":field: :before → :after" few: "Zmieniono :count pola" many: "Zmieniono :count pól" other: "Zmieniono :count pola" |
+| Created / deleted markers | `history.created` / `history.deleted` | (created) / (deleted) | (utworzono) / (usunięto) |
+| Row state `disabled` | `history.state_undone` | Undone | Cofnięte |
+| Filters | `history.filter_via` / `_when` / `_who` | Via / When / Who | Skąd / Kiedy / Kto |
+| Preview subtitle | `history.preview` | Change details | Szczegóły zmiany |
+| P5 columns | `history.diff_field` / `_before` / `_after` | Field / Before / After | Pole / Przed / Po |
+| Undo confirm | `history.undo_confirm` | Undo this change? The values before it are restored, and the undo is recorded as a new change. | Cofnąć tę zmianę? Przywrócone zostaną wcześniejsze wartości, a cofnięcie zostanie zapisane jako nowa zmiana. |
+| Undo success | `history.undone` | Change undone. | Cofnięto zmianę. |
+| Record action: undo batch | `history.undo_batch` | Undo the whole batch | Cofnij całą partię |
+| Undo batch confirm | `history.undo_batch_confirm` | one: "Undo all :count change of this batch? It is undone as a whole or not at all." other: "Undo all :count changes of this batch? They are undone as a whole or not at all." | one: "Cofnąć :count zmianę z tej partii? Cofa się całość albo nic." few: "Cofnąć wszystkie :count zmiany z tej partii? Cofa się całość albo nic." many: "Cofnąć wszystkie :count zmian z tej partii? Cofa się całość albo nic." other: "Cofnąć wszystkie :count zmiany z tej partii? Cofa się całość albo nic." |
+| Undo batch success | `history.batch_undone` | Batch undone. | Cofnięto partię. |
+| Refused: newer change (403 Message) | `history.undo_stale` | “:field” has changed since: :author changed it on :time. Undoing would overwrite that. Nothing was undone. | „:field” zmieniło się od tego czasu: :author zmienił je :time. Cofnięcie nadpisałoby tę zmianę. Nic nie cofnięto. |
+| Refused: would create a loop (403 Message) | `history.undo_loop` | Undoing would create a redirect loop: :path. Nothing was undone. | Cofnięcie utworzyłoby pętlę przekierowań: :path. Nic nie cofnięto. |
+| Refused: record gone (403 Message) | `history.undo_gone` | The :type this change belongs to no longer exists. Nothing was undone. | :type, którego dotyczy ta zmiana, już nie istnieje. Nic nie cofnięto. |
+
+### Import and export (singleton screen + E6)
+
+| Element | Key | EN | PL |
+|---------|-----|----|----|
+| Screen title | `transfer.title` | Import and export | Import i eksport |
+| Delimiter field | `transfer.delimiter` | CSV delimiter | Separator CSV |
+| Delimiter options | `transfer.delimiter_comma` / `_semicolon` | Comma (,) / Semicolon (;), as Excel uses in Polish | Przecinek (,) / Średnik (;), jak w polskim Excelu |
+| Delimiter comment | `transfer.delimiter_comment` | Files are UTF-8 with a byte order mark, so Excel opens Polish characters correctly. | Pliki są w UTF-8 ze znacznikiem BOM, więc Excel poprawnie otwiera polskie znaki. |
+| Widget label (E6 field label) | `transfer.widget` | Files | Pliki |
+| Element: export heading | `exportHeading` | Export | Eksport |
+| Element: import heading | `importHeading` | Import | Import |
+| Element: dataset label | `dataset` | Data | Dane |
+| Element: dataset options | `dsRedirects` / `dsRules` / `dsRecords` | Redirects / URL rules / SEO of records | Przekierowania / Reguły adresów / SEO rekordów |
+| Element: choose file | `chooseFile` | Choose CSV file | Wybierz plik CSV |
+| Element: no file | `noFile` | No file chosen | Nie wybrano pliku |
+| Element: summary | `summary` | :new new, :changed changed, :unchanged unchanged, :refused refused | Nowe: :new, zmienione: :changed, bez zmian: :unchanged, odrzucone: :refused |
+| Element: badges | `bNew` / `bChanged` / `bUnchanged` / `bRefused` / `bWarning` | New / Changed / Unchanged / Refused / Warning | Nowy / Zmieniony / Bez zmian / Odrzucony / Ostrzeżenie |
+| Element: table columns | `colLine` / `colChange` / `colAddress` / `colField` / `colBefore` / `colAfter` / `colNote` | Line / Change / Address / Field / Before / After / Note | Wiersz / Zmiana / Adres / Pole / Przed / Po / Uwagi |
+| Element: row cap | `rowCap` | Showing the first 200 of :count rows. All rows are applied. | Pokazano pierwsze 200 z :count wierszy. Zastosowane zostaną wszystkie. |
+| Element: refused blocks apply | `refusedBlocks` | Fix the refused rows in the file and preview it again. Nothing is applied while a row is refused. | Popraw odrzucone wiersze w pliku i ponownie wyświetl podgląd. Nic nie zostanie zastosowane, dopóki jakiś wiersz jest odrzucony. |
+| Element: nothing to apply | `nothing` | The file changes nothing. | Plik niczego nie zmienia. |
+| Element: discard | `discard` | Discard preview | Odrzuć podgląd |
+| Element: file too large | `tooLarge` | This file is larger than :limit. Split it into smaller files, or import it through MCP. | Ten plik jest większy niż :limit. Podziel go na mniejsze pliki lub zaimportuj przez MCP. |
+| Element: open history | `openHistory` | Open History | Otwórz Historię zmian |
+| Server: applied (toast) | `transfer.applied` | one: "Applied :count change as one batch. You can undo it in History." other: "Applied :count changes as one batch. You can undo them in History." | one: "Zastosowano :count zmianę jako jedną partię. Możesz ją cofnąć w Historii zmian." few: "Zastosowano :count zmiany jako jedną partię. Możesz je cofnąć w Historii zmian." many: "Zastosowano :count zmian jako jedną partię. Możesz je cofnąć w Historii zmian." other: "Zastosowano :count zmiany jako jedną partię. Możesz je cofnąć w Historii zmian." |
+| Server: stale preview (action error) | `transfer.stale` | The data changed after the preview. Preview the file again before applying. | Dane zmieniły się po podglądzie. Wyświetl podgląd pliku ponownie przed zastosowaniem. |
+| Server: unreadable file | `transfer.unreadable` | The file could not be read as CSV. Check the delimiter and that the first row holds the column names. | Nie udało się odczytać pliku jako CSV. Sprawdź separator i czy pierwszy wiersz zawiera nazwy kolumn. |
+| Server: missing column | `transfer.missing_column` | The file has no “:column” column. Download an export to see the expected columns. | W pliku brakuje kolumny „:column”. Pobierz eksport, aby zobaczyć oczekiwane kolumny. |
+| Row note: loop (refused) | `transfer.row_loop` | Creates a redirect loop: :path | Tworzy pętlę przekierowań: :path |
+| Row note: chain (warning) | `transfer.row_chain` | Creates a chain: :path | Tworzy łańcuch: :path |
+| Row note: canonical redirected (warning) | `transfer.row_canonical` | The canonical URL redirects to “:target” | Adres kanoniczny przekierowuje na „:target” |
+| Row note: no owner, URL rule created | `transfer.row_rule` | No record at this address: a URL rule is created | Pod tym adresem nie ma rekordu: zostanie utworzona reguła adresu |
+| Row note: invalid value (refused) | `transfer.row_invalid` | :field: :message | :field: :message |
+
+### Destructive actions
+
+| Action | Confirmation approach | Copy |
+|--------|----------------------|------|
+| Delete a redirect / selected redirects | Existing danger confirm (form footer, list toolbar) | `redirects.delete_confirm`, `redirects.delete_selected_confirm` |
+| Delete a URL rule / selected rules | Existing danger confirm | `rules.delete_confirm`, `rules.delete_selected_confirm` |
+| Remove 404 log entries | Existing danger confirm (toolbar delete) | `notfound.delete_selected_confirm` |
+| Mark as gone (410), record and bulk | Existing confirm, `Danger: true`, because a 410 tells search engines to drop the URL | `notfound.gone_confirm`, `notfound.gone_selected_confirm` |
+| Undo a change / a batch | Existing confirm, not danger (undo is itself undoable) | `history.undo_confirm`, `history.undo_batch_confirm` |
+| Apply an import | The preview is the confirmation. "Apply" is enabled only after a fresh preview with zero refused rows; no extra dialog | E6 `applyCount` |
+| Change the domain form, trailing slash or lowercase | No dialog: Save is the confirmation, as for every settings field. The field comment states the 301 consequence | `settings.url_change_comment` |
+| Turn an AI bot off | No dialog (reversible switch); the comment states the effect | `bots.*_comment` |
+| Save robots.txt with `Disallow: /` for `*` in production | No dialog. The served-file partial shows the result after save, and the change is undoable in History | — |
+
+---
+
+## Component Contracts
+
+**Focal points.** SEO tab: the E3 result preview at the top of the tab, then the title field under it. Redirects, URL rules, 404 log, History: the list table (the 404 log's first column, sorted by hits). robots.txt: the E4 editor, with the lint panel directly below it. Import and export: the E6 preview table and its "Apply :count changes" button, the only primary button on the screen. SEO settings: the active tab's first field (General: the title template).
+
+### C1. D-91 extended forms: what the SPA renders (framework-visible, no SPA change)
+
+- cabana sends the compiled field list **in final order**. The SPA derives tabs by first appearance, exactly as today (`FormView` `tabs`), and **adds no ordering logic**.
+- Fields without a `tab` form the default tab, which the SPA always puts first (`DEFAULT_TAB`, existing `unshift`). So the contract is: an extension tab anchored `Before` the default tab is a **boot error**, and the default tab stays first. A form with no tabs that receives an extension tab gains tabs: its own fields under `form.tab_default` ("Basic" / "Podstawowe", existing), then the new tab. Journal Category and Tag forms render exactly this way: "Basic | SEO".
+- **Zones.** D-91 names zones (outside the tabs, primary tabs, secondary tabs). The SPA renders only one tab zone today, and `fields.yaml` has no `tabs:`/`secondaryTabs:` blocks. Phase 32 renders placement in that zone only. Any other zone value stops the boot with the clear-error rule of D-91 until a later phase renders it (Open question Q-1).
+- A hidden owner field is absent from the schema. A tab that loses all its fields disappears (existing first-appearance rule).
+- `span` (left, right, full) keeps working: inserted fields sit in the same two-column `FormGrid`. An extension field `span: left` after an owner `span: left` field starts a new row (existing grid flow).
+- An extension field's 422 error counts in its tab's error badge, and its tab is activated on the first invalid field (existing).
+- Extension `Save` hooks may answer **save notices per field**. They render as the existing `FormField` notice box (`summer-callout summer-callout--warning`, `role="status"`). This is how D-18's automatic-redirect notice and D-19's chain notice reach the editor without new SPA code (recommendation to the D-91 seam; Open question Q-2).
+- **Permission.** The SEO tab's fields are removed from the schema per request when the admin lacks `golem15.seo.manage` (the same per-request removal pattern as 19.6.1 D-03). The save then neither sends nor changes SEO values. When the per-type "Show the SEO tab" switch is off, the tab is removed per request as well, and stored values stay in effect.
+
+### C2. Navigation and settings placement
+
+- `pact.NavigationItem` `seo` has label `nav.seo`, icon `search`, permission `golem15.seo.manage`, order 600 (after content plugins), and controller `golem15.seo.redirects`, so the item lands on Redirects. Side menu, in this order: Redirects (`arrow-right`), 404 log (`triangle-alert`), URL rules (`link`), robots.txt (`file-text`), History (`clock`), Import and export (`download`).
+- `pact.SettingsItem` `seo_settings`: category `settings.category`, label `settings.label`, description `settings.description`, icon `search`, permission `golem15.seo.manage`, singleton form `models/settings/fields.yaml`, order 100.
+- An admin without `golem15.seo.manage` sees neither the navigation item nor the settings tile (existing filters).
+
+### C3. SEO tab (injected into every registered content type, D-01, D-04, D-06, D-08, D-09)
+
+Placement: a new tab `golem15.seo::lang.tab.seo`, no anchor (end of the form, after the owner's last tab). On Journal Post: "Edit | Categories | Manage | SEO". Fields in this order:
+
+| # | Field | Type | Span | Context | Notes |
+|---|-------|------|------|---------|-------|
+| 1 | `seo_status` | partial P1 | full | update | Renders nothing when no callout applies (then it adds no visible row). Callouts, at most one each, in this order: URL rule override (`summer-callout--warning`, title + text listing the overridden properties); canonical target noindex; canonical target redirected |
+| 2 | `seo_snippet` | widget E3 | full | create, update | `fill: [seo_title, seo_description, seo_keyphrase]`, action `seo_snippet`. Label `field.snippet` |
+| 3 | `seo_title` | `mlseotext`, `measure: title` | full | create, update | Comment `field.title_comment` |
+| 4 | `seo_description` | `mlseotext`, `measure: description` | full | create, update | Comment `field.description_comment` |
+| 5 | `seo_keyphrase` | mltext | left | create, update | Comment `field.keyphrase_comment` |
+| 6 | `seo_canonical` | mltext | right | create, update | Comment `field.canonical_comment`. Server 422 when not a path or an http(s) URL: existing `validation.url` phrase |
+| 7 | `seo_robots` | `mlseorobots`, `scope: record` | full | create, update | Comment `field.robots_comment` |
+| 8 | `seo_og_title` | mltext | full | create, update | Comment `field.og_title_comment` |
+| 9 | `seo_og_description` | mltextarea, `size: small` | full | create, update | — |
+| 10 | `seo_og_image` | fileupload, `mode: image`, `maxFiles: 1`, `imageWidth: 240`, `imageHeight: 126` | full | create, update | Comment `field.og_image_comment`. One image per record, shared by every locale (Open question Q-5) |
+| 11 | `seo_effective` | partial P2 | full | update | Label `field.effective` |
+
+- Every ML field shares the form's locale switcher (existing `summer:ml-locale` broadcast). E3 listens to the same window event, so the preview always shows the locale being edited.
+- Phase 33 adds structured-data, primary-category and pagination fields to this tab through the same placement rules (`After: seo_robots` / `After: seo_og_image`). Phase 32 reserves these anchor names, so they must not be renamed.
+
+### C4. E1 `golem15-seo-text` (field types `mlseotext` / `seotext`)
+
+- **Control:** `measure: title` renders a 44px ``; `measure: description` renders a `