From d661e1de95d1ef20965ca3dc8297a18048aabc17 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 26 Oct 2026 19:45:16 +0100 Subject: [PATCH] docs(32): UI design contract Admin surfaces for sm-seo-plugin: SEO tab, six custom elements, redirects, 404 log, URL rules, robots.txt, history, CSV import/export. Keyphrase stem matching decided (Q-4). --- .../phases/32-seo-foundation/32-UI-SPEC.md | 953 ++++++++++++++++++ 1 file changed, 953 insertions(+) create mode 100644 .planning/phases/32-seo-foundation/32-UI-SPEC.md 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 `