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).
This commit is contained in:
953
.planning/phases/32-seo-foundation/32-UI-SPEC.md
Normal file
953
.planning/phases/32-seo-foundation/32-UI-SPEC.md
Normal file
@@ -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 `<input type="text">`; `measure: description` renders a `<textarea rows="3">` (min-height 96px, `resize: vertical`). Box styles copy `controlClass`: 1px `--c-border-strong` border (`--c-danger` when the `invalid` attribute is set), 10px radius, 14px horizontal padding, `--c-surface` background, `--c-text` 14px/400. Focus shows a `--c-primary` border plus a 3px `--c-ring` shadow. The `disabled` attribute makes the control `readonly` with no counter changes.
|
||||
- **Counter line** (8px below, 13px, `display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap`):
|
||||
- start: status icon (14px) and status text;
|
||||
- end: `count` text, muted, `font-variant-numeric: tabular-nums`.
|
||||
- It updates on every `input` event, synchronously (no debounce).
|
||||
|
||||
| Status | Rule (title) | Rule (description) | Icon / colour |
|
||||
|--------|--------------|--------------------|---------------|
|
||||
| empty | value is empty after trimming | value is empty after trimming | `info`, `--c-muted` |
|
||||
| short | < 30 characters | < 120 characters | `triangle-alert`, `--c-text` |
|
||||
| long | > 580px | > 920px (desktop limit) | `triangle-alert`, `--c-text` |
|
||||
| good | otherwise | otherwise | `circle-check`, `--c-ok-text` |
|
||||
|
||||
- Pixels decide "long"; the character count is advisory. `:max` in the counter is 580 (title) or 920 (description).
|
||||
- **Accessibility:** the control's `aria-describedby` points to the counter line. The element owns a visually hidden `aria-live="polite"` node that announces **only a change of status** ("Too long: …"), never each keystroke.
|
||||
- **Value:** a plain string (per locale inside MLField). The element emits `summer-change` with the trimmed-right value on `input`.
|
||||
|
||||
### C5. E2 `golem15-seo-robots` (field types `mlseorobots` / `seorobots`)
|
||||
|
||||
- **Layout:** row 1 holds two segmented radio groups side by side (24px gap; they stack under 600px): "Indexing" (Inherit | Index | Noindex) and "Links" (Inherit | Follow | Nofollow). Each group name is a 13px muted caption above its track.
|
||||
- The track and segments copy the PermissionEditor radio control: track `border 1px --c-border`, `--c-subtle`, 12px radius, 4px padding, 4px gap; segments 34px tall, 9px radius, 16px horizontal padding, 13px muted. Checked: 14px/600.
|
||||
- Inherit / Site default checked: `--c-surface` with `--shadow-tab`. Index / Follow checked: `--c-ok-bg` / `--c-ok-text`. Noindex / Nofollow checked: `--c-danger-soft` / `--c-danger`.
|
||||
- `role="radiogroup"` / `role="radio"`, with arrow keys moving and selecting.
|
||||
- **Row 2:** a disclosure button "Advanced directives" (14px/600, `chevron-down` rotating 180° with no animation, `aria-expanded`), followed by the muted 13px summary `advancedSet` when any advanced value is set. The panel is open on load when any advanced value is set. The panel is a two-column grid (16px gap, one column under 600px):
|
||||
- max-snippet (44px number input, empty or an integer ≥ -1, hint `maxSnippetHint`);
|
||||
- max-image-preview (44px select);
|
||||
- max-video-preview (44px number input);
|
||||
- unavailable_after (44px `<input type="date">`);
|
||||
- then three checkboxes in one full-width column: noarchive, nosnippet, noimageindex (20px box, 5px radius, `--c-primary` when checked).
|
||||
- An invalid number shows a 13px `--c-danger` line `invalidNumber` under its input; the element keeps the last valid value.
|
||||
- **Value grammar:** comma-separated directives in fixed order: `index|noindex`, `follow|nofollow`, `noarchive`, `nosnippet`, `noimageindex`, `max-snippet:N`, `max-image-preview:none|standard|large`, `max-video-preview:N`, `unavailable_after:YYYY-MM-DD`. Inherit and "not set" contribute nothing. **An empty string means inherit everything.** The server Decode checks the grammar and answers a `FormFieldError` with existing validation copy on a malformed value (only reachable through MCP or import).
|
||||
- `scope: type` labels the Inherit segment `siteDefault`. In the settings, "Site default" means index, follow.
|
||||
|
||||
### C6. E3 `golem15-seo-serp` (result preview widget)
|
||||
|
||||
- **On connect:** the element dispatches `summer-action` with `payload {op: "context"}`. The action answers `data`:
|
||||
- `locales[]` in order (default first), each with `{locale, url | null, h1, siteName, titleTemplate, defaultTitle, defaultDescription | null, descriptionSource, bodyWords | null}`;
|
||||
- `host`.
|
||||
- `fill` is empty and there is no message. While waiting, the element shows a skeleton card (600 × 132px, 12px radius, `--c-skel`) and `aria-busy="true"`.
|
||||
- **Header row** (space-between):
|
||||
- start: Desktop | Mobile segmented control, the same track as E2 with neutral checked state (surface + shadow), default Desktop;
|
||||
- end: `language` text, 13px muted.
|
||||
- **Result card** (exception E-1): `max-width` 600px on Desktop and 360px on Mobile, 16px padding, 1px `--c-border`, 12px radius, mimic palette.
|
||||
- Line 1: a 26px circle (`--c-subtle` fill, 1px `--c-border` ring) holding the site name's first letter (14px/600 `--c-muted`). Next to it, two stacked lines: the site name (14px) and the breadcrumb URL (12px), built from host plus path segments joined by ` › `.
|
||||
- Line 2: the title, truncated at 580px. On Mobile it may wrap to two lines within 360px; truncation uses the same 580px budget across both lines.
|
||||
- Line 3: the description, truncated at 920px (Desktop) or 680px (Mobile).
|
||||
- The title shown is the effective one: the SEO title if set, otherwise the type's title template (or the general template) applied to `defaultTitle` and `siteName`. The description is the SEO description, otherwise `defaultDescription`, otherwise the line `noDescription` in muted italic.
|
||||
- The card updates synchronously on every `fill-values` change.
|
||||
- **Source lines** under the card (13px muted, 8px gap): `fromTemplate` when the title is empty; `fromContent` when the description comes from content; `noUrl` when `url` is null; `savedNote` always on update.
|
||||
- **Duplicate check:** 800ms after the last change of `seo_title` or `seo_description` in `fill-values` (and once after context), the element dispatches `{op: "duplicates", locale, title, description}`. The action answers `data {title: [{label, url}], description: [...]}`.
|
||||
- While pending, the check area shows `checking`.
|
||||
- Each non-empty list renders a `triangle-alert` line `dupTitle` / `dupDescription`. `:list` holds up to three labels, each a link (`target="_blank"`, `rel="noopener"`) to the duplicate's admin form, then `andMore`.
|
||||
- Because WidgetField ignores events while `busy`, the element queues at most one pending request and sends it when `busy` is removed.
|
||||
- **Keyphrase checks** (only when `seo_keyphrase` for the shown locale is non-empty): a 14px/600 heading `keyphrase`, then up to five lines with `circle-check` (`--c-ok-text`) or `triangle-alert` (`--c-text`): title, H1, description, URL, text length.
|
||||
- Matching is case-insensitive and diacritics-insensitive, word by word, with simple stem matching for inflection (Q-4, decided 2026-10-26): two words match when they are equal, or when both have at least 5 characters and share a prefix of at least max(5, length of the shorter word − 3) characters ("grzyby" matches "grzybów"). A phrase matches when its words appear in order, each matching one word of the text. For the URL check, spaces equal hyphens. Some false positives are accepted.
|
||||
- The text-length line is omitted when `bodyWords` is null (a content type whose descriptor supplies no body). Open question Q-4 covers inflection.
|
||||
- **Failure:** a failed `context` action shows a 13px `--c-danger` line with `circle-alert` and the server message (the SPA also shows its danger toast). The card still renders from `fill-values` alone, without URL and template.
|
||||
|
||||
### C7. URL rules (list + form)
|
||||
|
||||
- `config_list.yaml`:
|
||||
- `title: rules.list_title`, `recordUrl` → update form, `recordsPerPage: 20`, `showCheckboxes: true`, `showSetup: true`;
|
||||
- toolbar `[create, delete]`, `noRecordsMessage: rules.list_empty`;
|
||||
- `defaultSort: { column: url, direction: asc }`.
|
||||
- Columns:
|
||||
- `url` (text, searchable, sortable);
|
||||
- `match` (text label);
|
||||
- `overrides` (text: the comma-joined property names the rule sets, e.g. "Title, Robots", server-built in `ListExtendRecords`);
|
||||
- `note` (text, searchable, invisible by default);
|
||||
- `updated_at` (datetime, sortable).
|
||||
- Filters: `match` (scope, two options).
|
||||
- Form fields in order:
|
||||
|
||||
| Field | Type | Span | Notes |
|
||||
|-------|------|------|-------|
|
||||
| `url` | text | left | Required. Label `rules.url`, comment `rules.url_comment_full` |
|
||||
| `match` | dropdown | right | Default `exact` |
|
||||
| `seo_status` | partial P1 | full | update; also shows canonical warnings |
|
||||
| `seo_snippet` | widget E3 | full | Same element; the context answers a single pseudo-locale with `url` = the rule URL (patterns: `url` null) and `h1` null (the H1 check is omitted) |
|
||||
| `title` | `seotext`, `measure: title` | full | Comment `field.title_comment_rule` |
|
||||
| `description` | `seotext`, `measure: description` | full | — |
|
||||
| `canonical` | text | left | Comment `field.canonical_comment` |
|
||||
| `robots` | `seorobots`, `scope: rule` | full | Comment `field.robots_comment_rule` |
|
||||
| `og_title` | text | left | — |
|
||||
| `og_description` | textarea, `size: small` | full | — |
|
||||
| `og_image` | fileupload image, `maxFiles: 1` | full | — |
|
||||
| `note` | textarea, `size: tiny` | full | Comment `rules.note_comment` |
|
||||
| `seo_effective` | partial P2 | full | update; for a pattern rule it shows the rule's own values, with the source "URL rule" |
|
||||
|
||||
|
||||
### C8. Redirects (list + form, D-16..D-19)
|
||||
|
||||
- List:
|
||||
- `title: redirects.list_title`, `recordUrl` → update form, 20 per page, checkboxes, setup;
|
||||
- toolbar `[create, delete]`, `noRecordsMessage: redirects.list_empty`;
|
||||
- `defaultSort: updated_at desc`;
|
||||
- row state `negative` (label `redirects.state_chain`) when the target is itself the source of another redirect.
|
||||
- Columns, in order:
|
||||
|
||||
| Column | Type | Visible | Search / sort |
|
||||
|--------|------|---------|---------------|
|
||||
| `source` | text | yes | searchable, sortable |
|
||||
| `target` | text (a 410 shows the existing empty-value dash) | yes | searchable |
|
||||
| `status` | text ("301", "302", "410") | yes | sortable |
|
||||
| `match` | text label | yes | sortable |
|
||||
| `origin` | text label | yes | sortable |
|
||||
| `updated_at` | datetime | yes | sortable |
|
||||
| `note` | text | invisible | searchable |
|
||||
|
||||
- Filters: `status` (scope: 301, 302, 410), `match` (scope), `origin` (scope), `chain` (scope: only / hide).
|
||||
- Form: `source` (text, left, required) + `match` (dropdown, right, default exact); `target` (text, full); `status` (dropdown, left, default 301) + `query` (dropdown, right, default ignore); `ignore_case` (checkbox, left, default off) + `trailing_slash` (checkbox, right, default on); `note` (textarea tiny, full).
|
||||
- Validation surfaces (all 422 field lines plus the existing count banner): loop on `target`; target required / target must be empty for 410 on `target`; invalid regex and invalid or duplicate source on `source`. A chain is saved and answers the save notice `redirects.chain_notice` on `target`.
|
||||
|
||||
### C9. 404 log (list + preview, D-20)
|
||||
|
||||
- List:
|
||||
- `title: notfound.list_title`, `recordUrl` → **preview**, 50 per page, checkboxes, setup;
|
||||
- toolbar `[delete]` (no create), `bulkActions: [gone]`, `noRecordsMessage: notfound.list_empty`;
|
||||
- `defaultSort: hits desc`;
|
||||
- row state `disabled` (label `notfound.state_resolved`) when a redirect or 410 now covers the URL.
|
||||
- Columns: `url` (text, searchable, sortable), `hits` (text, sortable), `last_hit` (datetime, sortable), `referer` (text, truncated by the existing cell rules), `first_seen` (datetime, invisible).
|
||||
- Filters: `status` (scope: open / resolved), `last_hit` (daterange).
|
||||
- Preview:
|
||||
- fields `url`, `hits`, `last_hit`, `first_seen`, `referer` (all read-only);
|
||||
- `preview: { headerPartial: hint }`, which shows `notfound.resolved_hint` in a plain `.summer-callout` (no modifier) when resolved, and nothing otherwise;
|
||||
- `recordActions: [redirect, gone]`. Both apply only while the entry is open.
|
||||
- `redirect` is prompted (`AdminActionPrompt` label `notfound.redirect_prompt`, placeholder `notfound.redirect_placeholder`, comment `notfound.redirect_comment`, `MaxLength: 2048`) and creates a 301 with origin "404 log".
|
||||
- `gone` is `Danger: true`.
|
||||
- Success: a toast, then the preview reloads showing the resolved hint, with no record actions left.
|
||||
|
||||
### C10. robots.txt screen (singleton controller, D-22, D-24, D-25)
|
||||
|
||||
- `golem15.seo.robots` implements `AdminSingleton`. Its form has tabs `robots.tab_file` and `robots.tab_bots`, and the existing singleton footer (Cancel, Save and close, Save; no Delete).
|
||||
- Tab "robots.txt":
|
||||
1. `staging` partial (full; renders the warning callout only outside production, otherwise nothing);
|
||||
2. `content` field type `robotstxt` (full);
|
||||
3. `tester` widget E5 (full, `fill: [content, <every bot switch>]`);
|
||||
4. `served` partial P4 (full).
|
||||
- Tab "AI bots": one `switch` per bot (default on), `span: left`/`right` alternating, in this order: search bots OAI-SearchBot, Claude-SearchBot, PerplexityBot; user-fetch bot ChatGPT-User; training bots GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, Meta-ExternalAgent. Label = token; comment by kind with `:operator`. The list is plugin data (Open question Q-6).
|
||||
- **E4 editor:**
|
||||
- a `<textarea>` with `spellcheck="false"`, `wrap="off"` (horizontal scroll), DM Mono 13px/1.6, min-height 320px, `resize: vertical`, 12px × 14px padding, and E1's box styles;
|
||||
- Tab is not captured (focus moves on);
|
||||
- below it, a lint panel (`--c-subtle`, 1px `--c-border`, 10px radius, 12px × 14px padding, 13px) listing up to 10 findings, each as `triangle-alert` + text (or `circle-check` + `clean` when none), then `moreFindings`;
|
||||
- the size meter `size`, end-aligned, muted, tabular numbers.
|
||||
- Lint runs synchronously on `input`. Findings are warnings and never block a save. Only content over 500 KB is refused, by the Decode `FormFieldError` (`robots.too_large`), with E4 showing `tooLarge` in `--c-danger`.
|
||||
- **E5 tester:**
|
||||
- an input row (wraps under 600px, 8px gap): address input (44px, flex 1, placeholder `urlPlaceholder`), crawler select (44px, min-width 200px: Googlebot, Googlebot-Image, Bingbot, then every AI bot token, then `*`), and the action button (42px outline, label from the action: `robots.test`);
|
||||
- Enter in the address input runs the test.
|
||||
- The result line below (14px, `role="status"`): `circle-check` `--c-ok-text` for allowed; `triangle-alert` `--c-text` for blocked; `stagingNote` (13px muted) when outside production; `unsaved` (13px muted) always.
|
||||
- An empty or non-`/` address shows `urlRequired` in `--c-danger` without calling the action.
|
||||
- **P4:** the saved file in `<pre>` inside `.summer-markdown`: the stored rules, the generated AI bot groups, and the `Sitemap:` line(s). Outside production it shows the file actually served (`User-agent: *` / `Disallow: /`) and, under it, a second `<pre>` headed "Production" with the production version.
|
||||
|
||||
### C11. SEO settings (singleton settings form, D-03, D-05, D-07, D-20b, D-21, D-23)
|
||||
|
||||
Tabs, in this order: General, one tab per registered content type (in signpost registration order, labelled with the type's label), Indexing, Addresses, Verification.
|
||||
|
||||
| Tab | Fields (type, span) |
|
||||
|-----|---------------------|
|
||||
| General | `staging` partial (full, outside production only; reuses the `robots.staging_*` copy); `site_name` (mltext, left); `twitter_site` (text, right); `title_template` (text, left, default `%title% \| %site%`); `home_template` (text, right, default `%site%`); `default_description` (mltextarea small, full); `default_og_image` (fileupload image, 1 file, full); `placeholders` partial P3 (full) |
|
||||
| Each content type `:type` | `type_<key>_inject` (switch, full, default on); `type_<key>_title_template` (text, left); `type_<key>_description_template` (text, right); `type_<key>_robots` (`seorobots`, `scope: type`, full; defaults per D-21: tags `noindex,follow`, authors `index,follow`, every other type empty = site default); `placeholders` partial P3 (full) |
|
||||
| Indexing | `query_params` (repeater, full, `titleFrom: name`; row fields `name` text left, `kind` dropdown right, `path` text full; seeded rows: `page` content; `utm_*`, `fbclid`, `gclid`, `msclkid` tracking; `sort` filter); `noindex_paths` (textarea small, left); `search_paths` (textarea small, right) |
|
||||
| Addresses | `url_host` (dropdown, left, default keep); `url_slash` (dropdown, right, default keep); `url_lowercase` (switch, full, default **off**) |
|
||||
| Verification | `verify_google` (text, left); `verify_bing` (text, right); `verify_yandex` (text, left); `verify_other` (repeater full: `name` text left, `content` text right); `verify_files` (repeater full, `titleFrom: filename`: `filename` text left, `body` textarea tiny right) |
|
||||
|
||||
- The per-type tabs come from the D-93 registrations at boot. The SEO plugin applies D-91 placement to its own settings form, or compiles the fields programmatically (planner's call); the rendered result is the table above.
|
||||
- The defaults on Addresses are "Do not change" and lowercase off. A fresh install therefore never 301s existing URLs until an editor opts in (Open question Q-7).
|
||||
|
||||
### C12. History (list + preview, D-13)
|
||||
|
||||
- List:
|
||||
- `title: history.list_title`, `recordUrl` → preview, 50 per page, no checkboxes, toolbar none, `noRecordsMessage: history.list_empty`;
|
||||
- `defaultSort: created_at desc`;
|
||||
- row state `disabled` (label `history.state_undone`).
|
||||
- Columns: `created_at` (datetime, sortable), `author` (text: admin name, or "MCP: <client>" for MCP), `via` (text label), `what` (text, searchable), `change` (text `history.change_cell`, one line, truncated by the existing cell rules), `batch` (text: the batch's first 8 characters, invisible by default).
|
||||
- Filters: `via` (scope), `created_at` (daterange), `author` (scope).
|
||||
- Preview:
|
||||
- read-only fields `created_at`, `author`, `via`, `what`, then `diff` partial P5 (a `.summer-table` with Field / Before / After; DM Mono for URL values; `(created)`/`(deleted)` markers, muted);
|
||||
- `recordActions: [undo, undo_batch]`. `undo` applies while not undone; `undo_batch` applies when the entry has a batch with at least one change not undone.
|
||||
- Both use the non-danger confirm. Refusals come back as 403 `Message` in a danger toast (`history.undo_stale`, `history.undo_loop`, `history.undo_gone`).
|
||||
- An undo writes a new entry (`via` = Undo) and marks the undone entries `Undone`.
|
||||
|
||||
### C13. Import and export (singleton controller + E6, D-12, D-13)
|
||||
|
||||
- `golem15.seo.transfer` implements `AdminSingleton`. Fields: `delimiter` (dropdown, left, default semicolon when the default locale is `pl`, otherwise comma) and `files` (widget E6, full, `fill: [delimiter]`). The footer Save stores the delimiter only.
|
||||
- **E6 layout** (two sections stacked with 24px gap, each headed by a 14px/600 label; section separator `border-top: 1px solid var(--c-border)` with 24px padding-top on the second):
|
||||
- **Export:** the dataset select (44px, min-width 240px) and a "Download CSV" outline button (42px) in a row with 8px gap. The action `{op: "export", dataset}` answers `data {filename, csv}`, and the element saves it through a Blob link (`seo-<dataset>-YYYY-MM-DD.csv`).
|
||||
- **Import:** the dataset select; a hidden `<input type="file" accept=".csv,text/csv">` behind an outline "Choose CSV file" button with the file name (or `noFile`, muted) next to it; then the "Preview import" outline button (label from the action, disabled until a file is chosen).
|
||||
- The element reads the file client-side. Over the size limit it shows `tooLarge` in `--c-danger` and does not call the action.
|
||||
- Otherwise `{op: "preview", dataset, csv}` returns `data {token, summary, rows[], total}`.
|
||||
- **Preview:**
|
||||
- the summary line (14px/600);
|
||||
- a `.summer-table` inside `.summer-table-wrap` (max-height 480px, vertical scroll) with columns Line (number cell), Change (24px pill badge: New `--c-ok-bg`/`--c-ok-text`; Changed `--c-sel`/`--c-text`; Unchanged `--c-subtle`/`--c-muted`; Refused `--c-danger-soft`/`--c-danger`; Warning `--c-sel`/`--c-text`), Address (DM Mono), Field, Before, After, Note;
|
||||
- `rowCap` when total > 200; refused rows sort first.
|
||||
- **Actions row** (end-aligned, 10px gap): "Discard preview" (ghost) and the primary "Apply :count changes".
|
||||
- Apply is disabled with `refusedBlocks` shown above it when any row is refused, and with `nothing` when the count is 0.
|
||||
- Apply sends `{op: "apply", token}`. Success shows the toast `transfer.applied`, clears the preview and shows an `openHistory` link to the History list.
|
||||
- The server refuses a token whose dry-run no longer matches the data (`transfer.stale`). The element then keeps the preview and shows the message in `--c-danger` above the actions.
|
||||
- Record-SEO rows address pages by URL (D-11). A row whose URL has no record creates a URL rule and carries the note `transfer.row_rule`.
|
||||
|
||||
### C14. Shared element rules (E1–E6)
|
||||
|
||||
- Light DOM only, with every CSS rule under the element tag. Colours come only from `--c-*` (except E-1). There is no global selector and no `!important`.
|
||||
- Strings come from the bundled en/pl dictionary by `ui-locale`. Numbers and dates are formatted with `Intl` in `ui-locale`.
|
||||
- Never store or read a token or cookie, and never call an admin route directly. Every server call is a `summer-action` through WidgetField (E3, E5, E6), or none at all (E1, E2, E4).
|
||||
- `disabled` makes every control read-only and hides action buttons (preview screens).
|
||||
- Focus-visible rules copy the base styles. Every control has a visible label or `aria-label`. Status lines use `role="status"` and never colour alone.
|
||||
- Under 600px, two-column rows stack, the result card uses the full width, and tables scroll horizontally inside `.summer-table-wrap`.
|
||||
- `prefers-reduced-motion`: no transitions beyond the 150ms colour change used by existing controls.
|
||||
|
||||
---
|
||||
|
||||
## UI Considerations
|
||||
|
||||
Applicable state considerations resolved for the elements below (probe matrix at the end of this section) (SEO tab, E1–E6, P1–P5, the four lists, the three singleton screens, settings). Empty-state and error copy lives in the Copywriting Contract.
|
||||
|
||||
| Category | Element(s) | Status | Resolution / Reason |
|
||||
|----------|------------|--------|---------------------|
|
||||
| empty | Redirects list | ✅ covered | `redirects.list_empty` names the three ways to add one; filtered views use the existing empty-search copy |
|
||||
| empty | URL rules list | ✅ covered | `rules.list_empty` explains what a rule is for |
|
||||
| empty | 404 log list | ✅ covered | `notfound.list_empty` |
|
||||
| empty | History list | ✅ covered | `history.list_empty` |
|
||||
| empty | SEO tab on a new record | ✅ covered | P1 and P2 are `context: update` and do not render; E3 shows `noUrl` and the template title; E1 shows the `empty` status |
|
||||
| empty | E3 without a description anywhere | ✅ covered | The card shows `noDescription` in muted italic |
|
||||
| empty | E4 with empty content | ✅ covered | Lint shows `clean`, the meter shows 0 KB, and P4 shows only the generated lines |
|
||||
| empty | E6 before a file is chosen | ✅ covered | `noFile`; "Preview import" disabled |
|
||||
| loading | Lists, forms, previews | ✅ covered | Existing skeletons; plugin elements show the existing WidgetField/PluginField skeleton until defined |
|
||||
| loading | E3 context and duplicate check | ✅ covered | Skeleton card until context; `checking` line while the duplicate action is pending |
|
||||
| loading | E5, E6 actions | ✅ covered | The existing `busy` attribute disables the element's buttons and shows `busy-label` on the pressed one |
|
||||
| error | Element script fails or times out | ✅ covered | Existing ExtensionFailure box with `extension.widget_failed` |
|
||||
| error | Widget action fails | ✅ covered | Existing danger toast; E3 also renders from `fill-values` alone; E6 keeps its state |
|
||||
| error | Save refused (422) | ✅ covered | Field lines, the count banner and the SEO tab error badge (existing); copy per C8, C7, C11 |
|
||||
| error | Record action / undo refused (403) | ✅ covered | Danger toast with the server `Message` (`notfound.has_rule`, `history.undo_*`, `redirects.loop`) |
|
||||
| error | Import refused rows / stale token / unreadable file | ✅ covered | Refused badges plus `refusedBlocks`; `transfer.stale`; `transfer.unreadable`, `transfer.missing_column` |
|
||||
| populated | SEO tab | ✅ covered | Order and spans per C3; values per locale under one switcher |
|
||||
| populated | Redirects list | ✅ covered | Six visible columns; chain rows carry the "Chain" badge |
|
||||
| populated | 404 log | ✅ covered | Sorted by hits, resolved rows badged |
|
||||
| populated | History | ✅ covered | Newest first; undone entries badged |
|
||||
| partial | Record with SEO in one locale only | ✅ covered | The other locale's ML values are empty (inherit); E1 shows `empty`, E2 shows Inherit, P2 shows template/content sources for that locale |
|
||||
| partial | Locale without an address (no translated slug) | ✅ covered | P2 shows `effective.no_url` for that locale; E3 shows `noUrl` |
|
||||
| partial | URL rule overriding a record | ✅ covered | P1 callout `status.rule_*` lists the overridden properties at the top of the tab |
|
||||
| partial | Admin without `golem15.seo.manage` | ✅ covered | No nav item, no tile, no SEO tab; saves leave SEO values untouched (C1) |
|
||||
| partial | Type with "Show the SEO tab" off | ✅ covered | Tab removed per request; stored values keep working (`settings.type_inject_comment`) |
|
||||
| partial | Non-production environment | ✅ covered | Staging callout on robots.txt and settings; P2 row note `status.staging`; E5 `stagingNote`; P4 shows served and production versions |
|
||||
| zero-one-many | Duplicate warnings in E3 | ✅ covered | Zero: no line; one/many: plural `dupTitle`/`dupDescription`, at most three links plus `andMore` |
|
||||
| zero-one-many | Bulk "Mark as gone" | ✅ covered | Enabled with a selection (existing); plural confirm; one already-covered row refuses the whole selection with `notfound.has_rule` |
|
||||
| zero-one-many | Import rows | ✅ covered | Zero changes: `nothing`, Apply disabled; one/many: plural `applyCount`; > 200 rows: `rowCap` |
|
||||
| zero-one-many | Content-type tabs in settings | ✅ covered | Zero registered types: only General, Indexing, Addresses, Verification; each registered type adds one tab in registration order |
|
||||
| zero-one-many | History batch undo | ✅ covered | `undo_batch` applies only to batched entries; plural confirm |
|
||||
| overflow | All lists | ✅ covered | Existing pagination and horizontal scroll; long URLs wrap by the existing cell rules |
|
||||
| overflow | E6 preview table | ✅ covered | `.summer-table-wrap` with max-height 480px and both-axis scroll; row cap 200 |
|
||||
| overflow | E4 long lines / many findings | ✅ covered | `wrap="off"` horizontal scroll; at most 10 findings plus `moreFindings` |
|
||||
| overflow | Settings repeaters | ✅ covered | Existing repeater rows with `titleFrom`, collapsed per row |
|
||||
| long-text | E3 title and description | ✅ covered | Truncated at the pixel budget with ` …`, the purpose of the widget |
|
||||
| long-text | URLs in P2, P5, E5 results, toasts | ✅ covered | `overflow-wrap: anywhere` (kit default); DM Mono for URL values in P5 and E6 |
|
||||
| long-text | E1 counter line at narrow width | 🧪 backstop | Element test: at 320px container width the status text and the count wrap onto two lines without overlap |
|
||||
| long-text | E2 segmented groups under 600px | 🧪 backstop | Element test: at 360px the two groups stack and no segment label is clipped in either language |
|
||||
| populated | E1 status thresholds | 🧪 backstop | Element unit test: given a mocked `measureText`, values at 29/30 characters and 580/581px produce short/good/long as specified, for both measures |
|
||||
| populated | E2 value grammar | 🧪 backstop | Element unit test: round-trip of every directive combination in fixed order; all-inherit emits ""; Go Decode test refuses malformed strings |
|
||||
| populated | E3 effective title | 🧪 backstop | Element unit test: empty SEO title applies the type template, then the general template, to `defaultTitle`; truncation cuts at a word boundary within 580px |
|
||||
| populated | E4 lint | 🧪 backstop | Element unit test: each lint rule fires on its sample line and none fires on a clean file |
|
||||
| populated | E6 apply gating | 🧪 backstop | Element unit test: a preview with one refused row disables Apply and shows `refusedBlocks` |
|
||||
| populated | C1 extended form rendering | 🧪 backstop | SPA test: a schema whose compiled order puts an extension field between two owner fields renders A, C, B, and a tab-less form plus an extension tab renders "Basic", then the new tab |
|
||||
| partial | Keyphrase inflection (Polish) | ✅ covered | Stem matching per C6 (Q-4 decided): "grzyby" matches "grzybów" |
|
||||
| populated | E3 keyphrase stem matching | 🧪 backstop | Element unit test: "grzyby"/"grzybów" match, "grzyb"/"grzbiet" do not, diacritics and case ignored, URL hyphens equal spaces |
|
||||
| partial | CSV size beyond the action caps | ⚠ unresolved | Payloads are capped at 64 KiB and responses at 256 KiB; planner treats `:limit` = 60 KB as the Phase 32 import limit pending Q-8 |
|
||||
|
||||
|
||||
### Probe coverage matrix
|
||||
|
||||
The ui-consideration probe (2026-10-26) raised 98 element × state considerations over 13 elements, with 0 unclassified. Each is resolved: E = explicit, by the row above or the cited contract; B = backstop, by the test named in a 🧪 row above; — = not raised by the probe. 92 explicit, 6 backstop, 0 unresolved. CSV size (Q-8) stays an assumption for Jakub and the planner (row above).
|
||||
|
||||
| Element | empty | loading | error | populated | partial | overflow | zero-one-many | long-text |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| SEO tab (C3) | E | E | E | B | E | E | — | E |
|
||||
| E1 text with counters | E | E | E | — | E | E | — | B |
|
||||
| E2 robots directives | E | E | E | — | E | — | — | B |
|
||||
| E3 result preview | E | E | E | B | E | E | E | E |
|
||||
| E4 robots.txt editor | E | E | E | B | E | E | E | E |
|
||||
| E5 robots tester | E | E | E | E | E | E | E | E |
|
||||
| E6 import and export | E | E | E | B | E | E | E | E |
|
||||
| Redirects (C8) | E | E | E | E | E | E | E | E |
|
||||
| URL rules (C7) | E | E | E | E | E | E | E | E |
|
||||
| 404 log (C9) | E | E | E | E | E | E | E | E |
|
||||
| History (C12) | E | E | E | E | E | E | E | E |
|
||||
| SEO settings (C11) | E | E | E | E | E | E | E | E |
|
||||
| robots.txt screen (C10) | E | E | E | E | E | E | E | E |
|
||||
|
||||
Resolutions not spelled out in a row above: E1, E2 and E4 have no server loading state (they render synchronously; the existing PluginField skeleton shows until the element is defined). E1's error state is the `invalid` attribute border (C4); E2's is `invalidNumber` (C5). E5's empty state is `urlRequired`, its partial state `stagingNote`/`unsaved`, and its input row wraps under 600px (C10). The settings form's empty state is the fresh-install defaults (C11, Q-7). On the robots.txt screen, the AI bot list grows as plugin data without a layout change (C10, Q-6).
|
||||
|
||||
---
|
||||
|
||||
## Open questions / assumptions
|
||||
|
||||
None of these block planning. Each states the default this contract uses.
|
||||
|
||||
| ID | Question | Default used here | Who decides |
|
||||
|----|----------|-------------------|-------------|
|
||||
| Q-1 | D-91 names zones (outside tabs, primary tabs, secondary tabs), but the SPA renders one tab zone and `fields.yaml` has no zone blocks | Phase 32 supports the tab zone only; other zone values are boot errors; an anchor `Before` the default tab is a boot error | Jakub (framework API) |
|
||||
| Q-2 | Can a D-91 extension's `Save` answer per-field save notices, and can an extension register the widget actions its fields need (E3 on another plugin's controller)? | Yes to both, as the D-91 seam's shape: notices reuse `FormFieldNotice` rendering; actions extend `AdminActionExtension` next to `RecordActions`. Without extension actions, E3 must fall back to a plugin field type whose `Load` returns the context, and the live duplicate check drops to save time | Jakub / planner |
|
||||
| Q-3 | Length thresholds | Title: short < 30 characters, long > 580px. Description: short < 120 characters, long > 920px desktop / 680px mobile. Measured in Arial 20px / 14px | Dawid |
|
||||
| Q-4 | Keyphrase matching for Polish inflection | **Decided (Dawid, 2026-10-26):** stem matching per C6 (shared prefix ≥ max(5, shorter word − 3)), case- and diacritics-insensitive | Dawid |
|
||||
| Q-5 | Social image per locale or per record | One per record, shared by all locales; title and description are per locale | Dawid |
|
||||
| Q-6 | AI bot list and classification | The ten tokens in C10 (the seven named in D-25 plus ChatGPT-User, Claude-SearchBot, Meta-ExternalAgent), all allowed by default; the list is plugin data and can grow without a UI change | Dawid |
|
||||
| Q-7 | URL normalisation defaults on a fresh install | "Do not change" for host and trailing slash, lowercase off, so enabling the plugin never mass-301s an existing site | Dawid |
|
||||
| Q-8 | CSV files larger than the widget action caps | 60 KB import limit, export within 256 KiB; larger jobs go through MCP. Lifting it needs a plugin download/upload route or a framework cap change | Jakub / planner |
|
||||
| Q-9 | Redirect query-string option for a source that itself contains `?` | Such a source matches its query exactly, whatever the option says | Dawid |
|
||||
| Q-10 | 404 log retention | Entries stay until deleted; no automatic pruning in Phase 32 | Dawid |
|
||||
| Q-11 | Publisher/organisation data (D-03, D-45), schema type, custom JSON-LD, primary category (D-44, D-86), llms.txt (D-25) | Phase 33: they have no consumer before the JSON-LD and sitemap output. Phase 33 adds them as a "Publisher" settings tab and SEO-tab fields after the reserved anchors | Jakub (phase split) |
|
||||
| Q-12 | Bulk "point chains to the final target" action on Redirects | Not in Phase 32; chains are badged and filterable, and the chain notice names the final target | Dawid |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none (shadcn not used) | not applicable |
|
||||
| third-party registries | none | not applicable |
|
||||
| npm dependencies (admin) | none added | not applicable |
|
||||
| npm dependencies (`sm-seo-plugin/assets`) | none at runtime. The element bundle is framework-free TypeScript; dev-only `typescript`, `vite`, `vitest` and `happy-dom` at the versions the journal Blog Editor pins (5.9.3, 7.3.5, 3.2.7, 20.11.6) | not applicable (same set as an approved sibling plugin) |
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [x] Dimension 1 Copywriting: PASS
|
||||
- [x] Dimension 2 Visuals: FLAG (addressed 2026-10-26)
|
||||
- [x] Dimension 3 Color: PASS
|
||||
- [x] Dimension 4 Typography: FLAG (addressed 2026-10-26)
|
||||
- [x] Dimension 5 Spacing: FLAG (addressed 2026-10-26)
|
||||
- [x] Dimension 6 Registry Safety: PASS
|
||||
- [x] Dimension 7 Inventory Provenance: PASS
|
||||
|
||||
**Approval:** approved 2026-10-26 (msd-ui-checker; FLAGs on dimensions 2, 4 and 5 addressed: focal points, type-size count, kit-value reuse note; 404 log action renamed "Create redirect…")
|
||||
Reference in New Issue
Block a user