docs(32): capture phase context
This commit is contained in:
362
.planning/phases/32-seo-foundation/32-CONTEXT.md
Normal file
362
.planning/phases/32-seo-foundation/32-CONTEXT.md
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
# Phase 32: SEO foundation - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-10-24
|
||||||
|
**Status:** Ready for planning. One open question for Jakub: the phase split, see "Open question" below.
|
||||||
|
**Participant:** Dawid (SEO specialist, delegated by Jakub for the SEO discussion). The discussion was in Polish; every decision below is recorded in English.
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## Phase Boundary
|
||||||
|
|
||||||
|
Search-engine optimisation built into SummerCMS, meant to be better suited to SEO than WinterCMS, for every application on the framework.
|
||||||
|
|
||||||
|
SummerCMS is headless: Go serves the admin and the JSON APIs, and Nuxt layers or apps render the public HTML. SEO is therefore split in two:
|
||||||
|
- **Backend (Go framework and shared plugins):** owns the SEO data, the rules and the admin. It serves the effective SEO of a URL to the frontend, along with robots.txt, the sitemaps, the redirect rules and the verification files. Every operation is reachable through MCP.
|
||||||
|
- **Nuxt layers:** render the data into `<head>`, JSON-LD, breadcrumbs, pagination and headings, enforce redirects and noindex on their routes, and guarantee server rendering.
|
||||||
|
|
||||||
|
The phase covers:
|
||||||
|
- per-record and global SEO;
|
||||||
|
- per-URL rules;
|
||||||
|
- canonical and meta robots;
|
||||||
|
- social tags;
|
||||||
|
- redirects;
|
||||||
|
- URL normalisation and strict URLs;
|
||||||
|
- robots.txt and AI bots;
|
||||||
|
- noindex policy;
|
||||||
|
- XML and HTML sitemaps;
|
||||||
|
- a page registry;
|
||||||
|
- pagination;
|
||||||
|
- structured data;
|
||||||
|
- headings;
|
||||||
|
- hreflang;
|
||||||
|
- an SSR guarantee;
|
||||||
|
- internal linking;
|
||||||
|
- media SEO;
|
||||||
|
- breadcrumbs;
|
||||||
|
- full MCP management;
|
||||||
|
- an audit crawler with an issue report.
|
||||||
|
|
||||||
|
Out of scope: e-commerce structured data and product or category shop pages (no shops yet); migrating `sm-bm-app` to SSR (it is internal, see D-71).
|
||||||
|
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Open question for Jakub: phase split
|
||||||
|
- **Q-01:** Dawid said the phase split is not his decision but Jakub's. The scope above is several weeks of work. Claude's recommendation is three consecutive phases. Each delivers a working slice that can ship to Płytarium, and every decision in this document applies to all three:
|
||||||
|
- **32 Foundation:**
|
||||||
|
- data model, SEO tab, global settings;
|
||||||
|
- meta, canonical, robots;
|
||||||
|
- redirects, URL normalisation, strict URLs;
|
||||||
|
- robots.txt, MCP tools, history.
|
||||||
|
- **33 Output:**
|
||||||
|
- XML and HTML sitemaps, page registry;
|
||||||
|
- JSON-LD, pagination, hreflang;
|
||||||
|
- headings, breadcrumbs;
|
||||||
|
- SSR rules in the layers.
|
||||||
|
- **34 Audit and content:**
|
||||||
|
- crawler and issue report;
|
||||||
|
- internal linking and auto-links;
|
||||||
|
- media (alt, file names, formats).
|
||||||
|
|
||||||
|
The alternative is one phase with many plans. Jakub decides before `/msd-plan-phase`; the planner must not split or merge phases on its own.
|
||||||
|
|
||||||
|
### 1. SEO data model
|
||||||
|
- **D-01:** **Per-record SEO is a framework mechanism.** A model opts in with one declaration. The framework adds a standard "SEO" tab to its admin form and stores the data in one shared table keyed by (model type, record id, locale). Every plugin, present and future, gets the same tab without its own migrations, and MCP has one place to read and write. cabana and pact have no cross-plugin form extension today (only `pact.HasAdminActionExtensions` for record actions), so this needs a new generic seam. — **Reversibility:** one-way — the shared table and the opt-in contract become a published framework API that every content plugin builds on.
|
||||||
|
- **D-02:** **Per-URL SEO rules** live on a separate screen, with exact URLs and patterns (e.g. `/blog?tag=*`). They cover pages with no record behind them: the home page, Nuxt pages, filter URLs. **Precedence: URL rule > record SEO > global defaults > values derived from content.** — **Reversibility:** costly — the precedence order is visible to editors and MCP clients and is baked into the effective-SEO resolver.
|
||||||
|
- **D-03:** **Global SEO settings** contain:
|
||||||
|
- title templates and defaults: a template such as `%title% | %site%`, a separate home-page template, the default meta description, the default OG image and the site name;
|
||||||
|
- organisation or person data for JSON-LD (D-43);
|
||||||
|
- search engine verification (D-05);
|
||||||
|
- default robots per content type (D-21).
|
||||||
|
- **D-04:** **Social tags inherit with optional overrides.** By default OG and Twitter take the title, description and image from SEO or from the content. A "Social" section in the SEO tab can override `og:title`, `og:description` and `og:image`. Twitter Card (`summary_large_image`) falls back to OG, and `twitter:site` is set in the global settings. There is no social card preview.
|
||||||
|
- **D-05:** **Search engine verification** supports two methods:
|
||||||
|
- meta tag codes (Google, Bing, Yandex, others), emitted on the home page;
|
||||||
|
- verification files (name and content) served from the site root, e.g. `/google1234.html`.
|
||||||
|
- **D-06:** **Canonical:**
|
||||||
|
- Every page is self-canonical by default: an absolute URL, with junk parameters (utm, fbclid and similar) stripped and pagination kept.
|
||||||
|
- Overrides are possible per record and per URL rule. An override may be relative or absolute and may point to another domain.
|
||||||
|
- The admin warns when a canonical points to a noindex or redirected page.
|
||||||
|
- **D-07:** **Global canonical rules use a query-parameter list.** Parameters marked as content (e.g. `page`) stay in the canonical; filter and sort parameters (e.g. `sort`, `color`) are dropped. The list can be refined per path pattern (e.g. `/katalog/*`). The same list drives the noindex of filter URLs (D-21).
|
||||||
|
- **D-08:** **Meta robots:**
|
||||||
|
- index/noindex and follow/nofollow switches, defaulting to "inherit";
|
||||||
|
- an advanced section: `max-snippet`, `max-image-preview`, `max-video-preview`, `noarchive`, `nosnippet`, `noimageindex`, `unavailable_after`;
|
||||||
|
- the same directives are also sent as an `X-Robots-Tag` header, so they work for PDFs and other files.
|
||||||
|
- **D-09:** **Editor aids in the SEO tab:**
|
||||||
|
- a live Google SERP preview (desktop and mobile, with pixel truncation);
|
||||||
|
- character and pixel counters for title and description, with warnings for too long, too short, empty, and duplicates of another record;
|
||||||
|
- a focus keyphrase with Yoast-style checks: the phrase in the title, H1, description and URL, plus text length.
|
||||||
|
|
||||||
|
### 2. MCP management
|
||||||
|
- **D-10:** **The whole of SEO is manageable through MCP.** Every admin operation in this phase has an MCP tool, built on the 19.7 `pier` module: admin routes become tools under the linked admin's permissions, and read grants see only read tools. Typical use: Dawid audits a site, then tells his Claude "noindex this page, 301 that one to this one".
|
||||||
|
- **D-11:** **Tools address pages by URL.** The server resolves the owner:
|
||||||
|
- if a record is behind the URL, the change goes into the record's SEO tab;
|
||||||
|
- otherwise a URL rule is created.
|
||||||
|
|
||||||
|
The answer says where the change was written, and the caller can force a URL rule.
|
||||||
|
- **D-12:** **Bulk changes:**
|
||||||
|
- Batch tools take a list of changes, e.g. 50 redirects from one audit.
|
||||||
|
- `dry_run` returns the diff and the conflicts: redirect loops and chains, noindex on a page that is in a sitemap, a canonical pointing to a redirected page.
|
||||||
|
- Applying a batch is all-or-nothing.
|
||||||
|
- **D-13:** **Read and audit tools:**
|
||||||
|
- the effective SEO of a URL, with the source of every value (URL rule, record, global or derived);
|
||||||
|
- the site issue report (D-14);
|
||||||
|
- CSV export and import with a preview, also in the admin;
|
||||||
|
- a change history with author, time, and before/after values, from both the admin and MCP, with undo of one change or of a whole batch.
|
||||||
|
- **D-14:** **The issue report combines two sources:**
|
||||||
|
- CMS data checks: duplicate or missing titles and descriptions, conflicting rules;
|
||||||
|
- a built-in crawler, run on demand or on a schedule, that fetches rendered pages from the sitemaps and from links. It checks status codes, H1/H2/H3, canonical, robots, JSON-LD, internal links, orphan pages, generic anchors and missing alt.
|
||||||
|
|
||||||
|
Results are available in the admin and over MCP. The crawler is the only way to check the Nuxt-rendered output and static frontend pages.
|
||||||
|
- **D-15:** **One SEO permission** covers all SEO operations: meta, rules, redirects, robots.txt, settings. It is not split per area.
|
||||||
|
|
||||||
|
### 3. Redirects, URL normalisation, strict URLs
|
||||||
|
- **D-16:** **Status codes: 301, 302 and 410 Gone.** No 307/308.
|
||||||
|
- **D-17:** **Source matching:**
|
||||||
|
- exact URLs (the default), wildcard patterns (`/old/*` → `/new/*`), and regex with capture groups;
|
||||||
|
- options: ignore case, trailing slash, and query string (ignore it or pass it through);
|
||||||
|
- evaluation order: exact, then patterns, then regex.
|
||||||
|
- **D-18:** **A slug or path change creates a 301** from the old URL to the new one and flattens existing chains (A→B, then B→C becomes A→C). Deleting or unpublishing a record creates nothing: the URL returns 404 and appears in the 404 log.
|
||||||
|
- **D-19:** **Loops are always refused**, whether they come from the admin, MCP or an import. Chains are saved with a warning and listed in the issue report.
|
||||||
|
- **D-20:** **404 log:** URL, hit count, last hit and referer. A redirect or 410 can be created from an entry with one click or through MCP. There are no per-rule hit counters and no validity dates.
|
||||||
|
- **D-20a:** **Where redirects execute is Claude's discretion.** The rules live in the CMS and are enforced by whatever serves the page: Nuxt Nitro middleware in a shared layer for frontend pages, Go for its own paths and for statically embedded sites.
|
||||||
|
- **D-20b:** **URL normalisation is set per site:**
|
||||||
|
- https always;
|
||||||
|
- www or bare domain;
|
||||||
|
- trailing slash or none;
|
||||||
|
- lowercase paths;
|
||||||
|
- double slashes, `/index.html` and similar collapse to the clean form.
|
||||||
|
|
||||||
|
Any other form reaches the final form in one 301 hop, never through a chain.
|
||||||
|
- **D-20c:** **Strict URLs.** WinterCMS answered 200 to `/<any-category>/<post-slug>` because it checked only the post slug, which created duplicates. In SummerCMS every URL segment (category, parent categories, author, locale prefix) must match the record. A URL whose segments do not match a record that exists 301s to the record's canonical URL. This also covers a post moving to another category. A wrong URL never answers 200. — **Reversibility:** costly — it changes how every layer route and every Journal API lookup resolves records.
|
||||||
|
|
||||||
|
### 4. robots.txt and noindex policy
|
||||||
|
- **D-21:** **Default robots per page kind** (editable per type in the global settings, and per record):
|
||||||
|
|
||||||
|
| Page kind | Robots |
|
||||||
|
|---|---|
|
||||||
|
| Login, register, password reset, every signed-in user page | noindex |
|
||||||
|
| Internal search results | noindex,follow |
|
||||||
|
| Filter and sort parameter URLs (D-07 list) | noindex,follow |
|
||||||
|
| Technical pages: cart and checkout (future), draft previews, thank-you pages, 404 and error pages | noindex |
|
||||||
|
| Tag archives | noindex,follow |
|
||||||
|
| Author pages | index,follow (always) |
|
||||||
|
| Pagination page 2+ | index,follow, self-referencing canonical |
|
||||||
|
- **D-22:** **robots.txt editor:**
|
||||||
|
- a text editor with syntax validation and a tester ("is this URL blocked for Googlebot?");
|
||||||
|
- the `Sitemap:` line is appended automatically;
|
||||||
|
- every non-production environment (staging, demo) always serves `Disallow: /` and `X-Robots-Tag: noindex`, whatever the stored content. — **Reversibility:** reversible — but the staging lock must never be disabled silently, since an indexed staging copy is costly to clean up.
|
||||||
|
- **D-23:** **Private frontend pages are marked automatically.** Every route that requires sign-in gets `noindex,nofollow` and an `X-Robots-Tag` header without per-page code. Path patterns in the admin cover public pages that should not be indexed (e.g. `/konto/*`, `/logowanie`). The user plugin's auth layer marks its own pages.
|
||||||
|
- **D-24:** **noindex, robots.txt and sitemaps stay consistent automatically.**
|
||||||
|
- Pages that are noindex, canonicalised elsewhere or redirected drop out of the XML and HTML sitemaps.
|
||||||
|
- The admin warns when a noindex page is blocked in robots.txt (Googlebot cannot see the noindex) and when a sitemap URL is blocked.
|
||||||
|
- **D-25:** **AI bots:**
|
||||||
|
- per-bot switches (GPTBot, OAI-SearchBot, ClaudeBot, Google-Extended, PerplexityBot, CCBot, Applebot-Extended and others), with AI search bots distinguished from training bots;
|
||||||
|
- all bots allowed by default;
|
||||||
|
- an optional `llms.txt` generated from the sitemap.
|
||||||
|
|
||||||
|
### 5. Sitemaps and page registry
|
||||||
|
- **D-30:** **Sitemap index split by type:**
|
||||||
|
- categories, tags, posts, CMS pages, and offer/service pages, each in its own sitemaps;
|
||||||
|
- files of 1000 URLs by default, configurable up to 50 000, so roughly 20 sitemaps can be added to Google Search Console and monitored one by one;
|
||||||
|
- stable file numbering (`sitemap-posts-1.xml`, …), sorted oldest first so new URLs land in the last file and earlier files do not shift;
|
||||||
|
- no `priority` and no `changefreq`;
|
||||||
|
- a real `lastmod`, from the actual content change rather than from any save of the record.
|
||||||
|
- **D-31:** **Additional sitemaps:**
|
||||||
|
- an image sitemap (`image:image`) with the featured image and the images in the body;
|
||||||
|
- an optional Google News sitemap for chosen categories or types;
|
||||||
|
- optional IndexNow, with a key generated in the admin, pinged on publish, slug change and deletion.
|
||||||
|
- **D-32:** **The new format replaces the PHP-parity mode of `sm-sitemap-plugin`.** The PHP byte parity, priority tiers and single file are removed, and every site moves to the new format, including grzybyfunkcjonalne.pl. This is a **breaking change on a shared core plugin, explicitly chosen by the user**. It needs a new plugin version, retires the PHP parity goldens, and migrates grzyby in the same delivery. Jakub should confirm this when reviewing this document. — **Reversibility:** one-way — sites drop the PHP format and their GSC sitemap submissions change.
|
||||||
|
- **D-33:** **Page registry in the CMS (admin and MCP).** Each entry holds the frontend URL, a page type (page, offer, service, landing), the SEO tab and a modification date. Nuxt pages read their SEO from it, and the sitemaps group by its type. The frontend can report its routes at build time so none are missed. It replaces "CMS pages" from WinterCMS (Winter.Pages was not ported). It is not a content plugin: page content stays in the frontend. — **Reversibility:** costly — the registry's API is consumed by every Nuxt app.
|
||||||
|
- **D-34:** **HTML sitemap:**
|
||||||
|
- It lists everything indexable (no 404s, no redirected URLs, no noindex), the same set as the XML sitemaps.
|
||||||
|
- It is linked from the footer of every page, so also from the home page.
|
||||||
|
- Its path is set in the admin; the defaults are `/mapa-strony` (pl) and `/sitemap` (en), with locale variants.
|
||||||
|
- The page component is provided by a Nuxt layer and can be overridden.
|
||||||
|
|
||||||
|
Grouping and paging are Claude's discretion: pages, offer, the category tree, posts, indexable tags, with large groups paged at about 100–200 links.
|
||||||
|
|
||||||
|
### 6. Pagination
|
||||||
|
- **D-40:** **URL form is chosen per site.** The default is `?page=N`. A site may choose a path form (`/blog/strona/2`), e.g. to keep WinterCMS URLs, and the other form 301s to the configured one.
|
||||||
|
- **D-41:** **Page 2 and later:**
|
||||||
|
- `index,follow` with a self-referencing canonical;
|
||||||
|
- a title suffix ("Blog – strona 2 z 14 | Site");
|
||||||
|
- a description suffix ("Strona 2 z 14");
|
||||||
|
- an H1 with the page number;
|
||||||
|
- the category's long text (pillar content) appears on page 1 only.
|
||||||
|
- **D-42:** **Mechanism:**
|
||||||
|
- Pagination always uses numbered, server-rendered `<a href>` links.
|
||||||
|
- "Load more" or infinite scroll is allowed only as an optional enhancement over those links, and it updates the URL to `?page=N`.
|
||||||
|
- `<link rel="prev/next">` goes in the head.
|
||||||
|
- `?page=1` (or `/strona/1`) 301s to the unnumbered URL. A page past the last one, and a garbage value (`?page=abc`, `-1`), return 404.
|
||||||
|
|
||||||
|
### 7. Structured data
|
||||||
|
- **D-43:** **JSON-LD is one `@graph` whose nodes link by `@id`, generated automatically.** Default mapping:
|
||||||
|
|
||||||
|
| Page type | Schema |
|
||||||
|
|---|---|
|
||||||
|
| Every page | `WebPage` + `BreadcrumbList` + references to `WebSite` and the publisher |
|
||||||
|
| Home | `WebSite` (+ `SearchAction` when the site has search) + the publisher |
|
||||||
|
| Post | `BlogPosting` (or `Article`/`NewsArticle`): author `Person`, publisher, image, dates, `wordCount`, `articleSection` |
|
||||||
|
| Category / tag | `CollectionPage` + `ItemList` |
|
||||||
|
| Author | `ProfilePage` + `Person` (`sameAs`) |
|
||||||
|
| Service / offer | `Service` (provider, `areaServed`, optional `Offer` with price) |
|
||||||
|
| Contact | `ContactPage` + `LocalBusiness`/`Organization` with address and opening hours |
|
||||||
|
| Page with the FAQ plugin | `FAQPage` |
|
||||||
|
| Post with a YouTube embed | additionally `VideoObject` |
|
||||||
|
- **D-44:** **Editor control in the SEO tab:**
|
||||||
|
- a schema type choice, e.g. BlogPosting, NewsArticle or Article; Service or Product;
|
||||||
|
- a field form that depends on the type, e.g. area, price and hours for Service;
|
||||||
|
- a "custom JSON-LD" field for advanced users, validated on save and merged into the graph.
|
||||||
|
- **D-45:** **The publisher is set in the global settings:**
|
||||||
|
- type: `Organization`, `LocalBusiness` with a subtype (address, geo, opening hours, phone), or `Person` for personal blogs;
|
||||||
|
- logo, `sameAs` (social profiles, Wikipedia, KRS) and contact data.
|
||||||
|
- **D-46:** **Structured data is validated in three places:**
|
||||||
|
- in the admin, against Google's rich-result required and recommended properties per type;
|
||||||
|
- in the crawler, on the rendered JSON-LD;
|
||||||
|
- through a "check in Rich Results Test" link per URL.
|
||||||
|
|
||||||
|
### 8. Headings
|
||||||
|
- **D-50:** **No H1 in body content.** The editor toolbar offers H2–H4 only. Any H1 in content (pasted, imported from WinterCMS, written through MCP) is converted to H2 on save, with a notice to the editor. Today `htmlGate.ts` and bluemonday `UGCPolicy` let H1 through.
|
||||||
|
- **D-51:** **A live heading outline in the editor** flags skipped levels, empty headings and headings that are only a link. The crawler checks the rendered pages: exactly one H1, no skipped levels, H1 consistent with the title. Nothing is rewritten silently apart from the D-50 conversion.
|
||||||
|
- **D-52:** **One heading convention in the Nuxt layers, enforced by their tests:**
|
||||||
|
- H1 only in the main content;
|
||||||
|
- post cards on listings are H2;
|
||||||
|
- sidebar and footer widgets use styled text, not content headings, so they do not disturb the outline.
|
||||||
|
|
||||||
|
### 9. hreflang
|
||||||
|
- **D-55:** **hreflang is emitted in both the head and the sitemaps, from one source** (the record's locale alternates, signpost `ResolvedMenuItem.Alternates`):
|
||||||
|
- `<link rel="alternate" hreflang>` in every page head;
|
||||||
|
- `xhtml:link` in the XML sitemaps;
|
||||||
|
- plus `<html lang>`, `og:locale` and `og:locale:alternate`.
|
||||||
|
- **D-56:** **`x-default` points to the default-locale version.**
|
||||||
|
- **D-57:** **A page without a translation has no hreflang for that locale.** Default-locale content is never served under another locale's URL.
|
||||||
|
- **D-58:** **Fix the Journal API translated-slug gap (Claude's discretion, required for D-55).** `findPostBySlugOrID` matches only the default-locale slug, so translated slugs do not resolve today.
|
||||||
|
|
||||||
|
### 10. SSR guarantee
|
||||||
|
- **D-70:** **Hard rule: indexable pages are always SSR or SSG.** `ssr: false` is allowed only on noindex routes. The Nuxt SEO layer fails the build when an indexable route has `ssr: false`. The crawler compares the raw (no-JS) HTML with the rendered HTML and reports content or links that appear only after JavaScript runs.
|
||||||
|
- **D-71:** **Server guarantees:**
|
||||||
|
- **Real HTTP status codes:** 404 rather than a soft-404 page with status 200, plus 410 and 301 per the rules.
|
||||||
|
- **Links:** all navigation (menus, pagination, cards, breadcrumbs) is server-rendered `<a href>`.
|
||||||
|
- **Content:** content in tabs, accordions and FAQs is in the HTML, only hidden by style.
|
||||||
|
- **Images:** native `loading="lazy"` with a real `src` and `alt`, explicit dimensions, and the featured image eager with `fetchpriority`.
|
||||||
|
- **D-72:** **`sm-bm-app`** is an internal application with no public indexable pages. It only gets noindex and `Disallow`, with no SSR migration.
|
||||||
|
|
||||||
|
### 11. Internal linking
|
||||||
|
- **D-80:** **One link per post card.** Only the title is a text link. The image is clickable for users but has no separate `<a>` in the HTML (or `tabindex=-1`, `aria-hidden`), so crawlers see one link with a good anchor. Dawid first proposed linking images with `rel=nofollow`, then chose this after the note that internal nofollow is only a hint and does not conserve PageRank. Post links carry no `rel` attribute.
|
||||||
|
- **D-81:** **No generic anchors.**
|
||||||
|
- In layer components, the card link is the post title, and any "read more" carries the title ("Czytaj: <title>").
|
||||||
|
- The content editor warns on links whose anchor is on an editable generic list ("kliknij tutaj", "tutaj", "więcej", "link", and the English equivalents).
|
||||||
|
- The crawler reports such links.
|
||||||
|
- **D-82:** **Smart internal linking:**
|
||||||
|
- **Related posts:** 3–6, computed from tags, category and optionally the focus keyphrase, server-rendered, plus the previous and next post in the category.
|
||||||
|
- **Phrase auto-linking:** a phrase → URL table. Only the first occurrence is linked, with at most N links per post, never to the post itself, never inside headings. Focus keyphrases can feed the table.
|
||||||
|
- **Editor suggestions:** proposed links with counters of inbound and outbound links.
|
||||||
|
- **Orphan report:** pages with no inbound links, with suggestions of where to link from.
|
||||||
|
- **D-83:** **Outbound links:**
|
||||||
|
- plain followed links by default;
|
||||||
|
- per-link rel checkboxes in the editor: `nofollow`, `sponsored`, `ugc`;
|
||||||
|
- a domain list that always gets nofollow or sponsored;
|
||||||
|
- external links open with `target=_blank` and `rel=noopener`.
|
||||||
|
|
||||||
|
### 12. Media and breadcrumbs
|
||||||
|
- **D-85:** **Media SEO:**
|
||||||
|
- `alt` is required in the media manager and when inserting into content; a "decorative" checkbox gives `alt=""`;
|
||||||
|
- SEO file names: a slug on upload, editable;
|
||||||
|
- automatic WebP/AVIF and `srcset` widths, keeping the original;
|
||||||
|
- a missing-alt report (CMS and crawler) and an MCP tool for bulk alt filling.
|
||||||
|
- **D-86:** **Breadcrumbs:**
|
||||||
|
- a visible breadcrumbs component in a Nuxt layer, rendered as `<a>` links from the same source as the `BreadcrumbList` JSON-LD;
|
||||||
|
- on by default on every page except home, and can be disabled or restyled;
|
||||||
|
- a post's trail goes through its primary category, selectable in the SEO tab.
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Where redirects execute (D-20a), HTML sitemap grouping and paging (D-34), the Journal translated-slug fix (D-58).
|
||||||
|
- Table, package and module names: a beach-themed framework module name per the repo rules, and whether the redirects, robots and global SEO pieces live in a new shared plugin (`sm-seo-plugin`, for example) or in the framework. The data model and admin seam (D-01) belong in the framework; editorial screens may live in a shared plugin. The research doc decides, and the planner records it.
|
||||||
|
- How the frontend fetches effective SEO: one resolver endpoint per URL, cached and purged through the existing site-cache purge.
|
||||||
|
- Crawler implementation (Go, in-process job via `conga`/River, politeness limits).
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<canonical_refs>
|
||||||
|
## Canonical References
|
||||||
|
|
||||||
|
**Downstream agents MUST read these before planning or implementing.**
|
||||||
|
|
||||||
|
### Prior phase decisions this phase builds on
|
||||||
|
- `.planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-CONTEXT.md`: D-02 Nuxt layers, D-07 SSR with swr/ISR, D-13 `@nuxtjs/seo` per-post meta (partly reversed by 17 D-15), D-15 URLs kept identical (one-way, now protected by D-20c and redirects).
|
||||||
|
- `.planning/phases/17-blog-companions/17-CONTEXT.md`: D-01 to D-03 the signpost menu-item-type registry, URL patterns and per-locale alternates (source for sitemaps, hreflang and breadcrumbs); D-11 to D-15 sitemap plugin behaviour that D-32 replaces.
|
||||||
|
- `.planning/phases/19.7-generic-mcp-module/19.7-CONTEXT.md`: the `pier` MCP module, tools bound to admin routes, read/write grant ceilings (basis for D-10 to D-13).
|
||||||
|
|
||||||
|
### Code seams
|
||||||
|
- `modules/pact/capabilities.go`: `HasSettings`/`SettingsItem` (~719-744), `MenuItemType`/`ResolvedMenuItem.Alternates` (~797-856), `AdminSingleton`.
|
||||||
|
- `modules/pact/README.md`: the list of form hooks; `HasAdminActionExtensions` (~149) is the only cross-plugin admin hook today.
|
||||||
|
- `modules/cabana/field_ml.go`: `TranslationWriter` and translatable field types (pattern for locale-keyed SEO data).
|
||||||
|
- `modules/cabana/settings.go`: settings form compilation.
|
||||||
|
- `modules/signpost/signpost.go`: the registry (`Activate`, `Resolve`).
|
||||||
|
- `modules/boardwalk/boardwalk.go:177`: the existing `X-Robots-Tag` on admin responses.
|
||||||
|
- `../sm-sitemap-plugin/`: `classes/generator.go`, `classes/serialize.go` (hreflang output), `routes.go`, `admin.go`, `models/settings.go`.
|
||||||
|
- `../sm-journal-plugin/`: `models/post.go` (hidden `metadata` jsonb), `controllers/api/posts.go` (`serializePost`), `controllers/api/posts_helpers.go` (`findPostBySlugOrID`, default-locale slug only), `classes/format_html.go` (bluemonday UGCPolicy, allows h1), `classes/menu_item_types.go`.
|
||||||
|
- `../sm-translate-plugin/classes/translator.go`: locale prefix and resolution.
|
||||||
|
- `../vue-journal-layer/`: `nuxt.config.ts` (`@nuxtjs/seo`, `canonicalQueryWhitelist`, sitemap source), `app/utils/seo.ts`, `app/components/Journal/Pagination.vue`, `app/journal-pages/*.vue`, `app/utils/htmlGate.ts`.
|
||||||
|
- Apps: `../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/nuxt.config.ts` (robots and sitemap config, swr), `../sm-demo-app/vue-demo-app/` (hand-written noindex on login and similar pages), `../sm-summercmsio-app/vue-summercmsio-app` (nuxt generate).
|
||||||
|
|
||||||
|
### Reference implementations
|
||||||
|
- `../examples/golem15-wintercms-starter/plugins/winter/pages/classes/page/fields.yaml`: WinterCMS `viewBag[meta_title]`/`meta_description` (the per-page SEO tab this phase improves on).
|
||||||
|
|
||||||
|
</canonical_refs>
|
||||||
|
|
||||||
|
<code_context>
|
||||||
|
## Existing Code Insights
|
||||||
|
|
||||||
|
### Reusable Assets
|
||||||
|
- **signpost registry:** resolves record URLs and per-locale alternates. It is the natural source for the owner of a URL (D-11), strict-URL checks (D-20c), sitemaps (D-30), hreflang (D-55) and breadcrumbs (D-86).
|
||||||
|
- **19.7 `pier`:** exposes admin routes as MCP tools under the linked admin's permissions. SEO admin routes become tools without a separate API (D-10).
|
||||||
|
- **`pact.HasSettings`:** singleton settings forms for the global SEO settings.
|
||||||
|
- **cabana translatable fields:** a pattern for locale-keyed SEO values.
|
||||||
|
- **Site-cache purge** (`POST /api/__site__/cache/purge`, shared by the layers): the way to invalidate rendered pages after SEO changes.
|
||||||
|
- **`conga` (River jobs):** for the crawler, IndexNow pings and sitemap rebuilds.
|
||||||
|
|
||||||
|
### Established Patterns
|
||||||
|
- **Compiled plugins and capability interfaces** (`pact.Has*`). The SEO opt-in follows this pattern.
|
||||||
|
- **Nuxt layers** distributed as pnpm git dependencies pinned by tag, with overridable components. SEO rendering and the SSR guard belong in a layer that apps extend.
|
||||||
|
- **The layers already render SSR.** `?page=N` pagination with canonical whitelisting exists; `rel=prev/next` is only on `<a>` links.
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
- **cabana form compilation:** needs a new generic seam that adds the SEO tab to opted-in models of other plugins. Nothing like it exists today.
|
||||||
|
- **Journal plugin:** opts its Post, Category and Tag into SEO, gets the translated-slug fix and the H1 → H2 conversion, and registers its schema types.
|
||||||
|
- **sm-sitemap-plugin:** rewritten to the split index (breaking, D-32).
|
||||||
|
- **Nuxt layers:** journal, menu and FAQ consume effective SEO, JSON-LD, breadcrumbs and the redirect middleware; the user plugin's auth pages get noindex.
|
||||||
|
- **Applications:** grzybyfunkcjonalne (sitemap migration and robots move from Nuxt to the CMS), demo, summercms.io, and Płytarium, the likely first consumer.
|
||||||
|
|
||||||
|
</code_context>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## Specific Ideas
|
||||||
|
|
||||||
|
- Dawid's audit workflow is the reference use case for MCP. After an audit he tells his own Claude client "this page noindex, that one 301 to this", and it happens through MCP with a dry-run preview.
|
||||||
|
- The WinterCMS bug to never repeat: `/<category>/<post>` answered 200 for any category segment (D-20c).
|
||||||
|
- Monitoring in GSC sitemap by sitemap is why the files are split by type and batched at 1000 (D-30).
|
||||||
|
- WinterCMS had a per-entry SEO tab plus a global SEO tab. SummerCMS keeps that shape and goes further (precedence rules, MCP, audit, structured data).
|
||||||
|
- The first site on SummerCMS is likely Płytarium, but the features are framework-wide and must not be shaped around one site.
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
- **Social card preview** (Facebook/LinkedIn/X) in the SEO tab: offered, not chosen.
|
||||||
|
- **Per-redirect hit counters and validity dates:** offered, not chosen.
|
||||||
|
- **307/308 redirects:** not chosen.
|
||||||
|
- **E-commerce structured data** (Product, offers on category pages): no shops yet.
|
||||||
|
|
||||||
|
### Reviewed Todos (not folded)
|
||||||
|
- `sitemap-xslt-browser-deprecation.md` (g15office w8kupqi3): replace the browser XSLT view of `/sitemap.xml`. The user chose to keep it as a separate backlog task. The D-32 rewrite of the sitemap plugin touches the same code, so the planner should note that the XSL output may be removed or kept there.
|
||||||
|
- `sitemap-plugin-port.md`: stale. The port was delivered in Phase 17 and should be closed separately.
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 32-seo-foundation*
|
||||||
|
*Context gathered: 2026-10-24*
|
||||||
448
.planning/phases/32-seo-foundation/32-DISCUSSION-LOG.md
Normal file
448
.planning/phases/32-seo-foundation/32-DISCUSSION-LOG.md
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
# Phase 32: SEO foundation - Discussion Log
|
||||||
|
|
||||||
|
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||||
|
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||||
|
|
||||||
|
**Date:** 2026-10-24
|
||||||
|
**Phase:** 32-seo-foundation
|
||||||
|
**Participant:** Dawid (SEO specialist, delegated by Jakub); discussion held in Polish, recorded in English
|
||||||
|
**Areas discussed:** 1. SEO data model, 2. MCP management, 3. Redirects, 4. robots.txt and noindex policy, 5. Sitemaps (XML boost, HTML sitemap), 6. Pagination, 7. Structured data per page type, 8. Heading structure, 9. hreflang basics, 10. SSR guarantee, 11. Internal linking (added by user), 12. URL normalisation, AI bots, media, breadcrumbs (added)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements stated by the user up front
|
||||||
|
|
||||||
|
- Per-record SEO tab plus global SEO settings, like WinterCMS, but better suited to search optimisation
|
||||||
|
- Journal records need meta title, meta description, canonical and meta robots
|
||||||
|
- Twitter Card besides Open Graph
|
||||||
|
- The whole of SEO must be manageable through MCP (e.g. after an audit: noindex this page, 301 that one to another)
|
||||||
|
- Structured data differs per page type: article, home page, service pages; no e-commerce yet
|
||||||
|
- HTML sitemap by default, linked from the home page
|
||||||
|
- Redirects 301 and 302; robots.txt editable in the admin
|
||||||
|
- Correct heading hierarchy: H1 first, then H2, nested H3
|
||||||
|
- noindex on filter pages, login and all signed-in user pages
|
||||||
|
- SSR so Googlebot sees all content and links
|
||||||
|
- (later) The whole of SEO must be manageable through MCP
|
||||||
|
- (later) Internal linking without generic anchors; smart default internal linking
|
||||||
|
- (later) No WinterCMS-style relative-URL duplicates (/<any-category>/<post> answering 200)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. SEO data model
|
||||||
|
|
||||||
|
**Q: Where per-record SEO lives**
|
||||||
|
|
||||||
|
Options presented: Framework mechanism; Columns on each model; Per-URL only
|
||||||
|
|
||||||
|
**User's choice:** Framework mechanism: a model opts in with one declaration; the framework adds a standard SEO tab and stores the data in one shared table keyed by (model type, record id, locale)
|
||||||
|
|
||||||
|
**Q: Per-URL override screen and precedence**
|
||||||
|
|
||||||
|
Options presented: URL > record > global; record > URL > global; No, record + global only
|
||||||
|
|
||||||
|
**User's choice:** Yes, with patterns; precedence URL rule > record SEO > global defaults > values derived from content
|
||||||
|
|
||||||
|
**Q: Global SEO settings contents**
|
||||||
|
|
||||||
|
Options presented: Title templates and defaults; Organisation/person data; Verification codes; Default robots per type
|
||||||
|
|
||||||
|
**User's choice:** All four: title templates and defaults (incl. home page template, default description, default OG image, site name); organisation/person data for JSON-LD; search engine verification codes; default robots per content type
|
||||||
|
|
||||||
|
**Q: Social tags**
|
||||||
|
|
||||||
|
Options presented: Inherit + optional override; Separate OG and Twitter fields; Automatic only
|
||||||
|
|
||||||
|
**User's choice:** Inherit from SEO/content with optional overrides of og:title, og:description, og:image; Twitter Card summary_large_image falls back to OG; twitter:site in global settings
|
||||||
|
|
||||||
|
**Q: Search engine verification**
|
||||||
|
|
||||||
|
Options presented: Meta tag + file; Meta tag only
|
||||||
|
|
||||||
|
**User's choice:** Meta tag codes (Google, Bing, Yandex, others) plus verification files (name + content) served from the site root
|
||||||
|
|
||||||
|
**Q: Canonical default and override**
|
||||||
|
|
||||||
|
Options presented: Auto self + override; Manual only
|
||||||
|
|
||||||
|
**User's choice:** Every page self-canonical by default (absolute, junk parameters stripped, pagination kept); field override per record and per URL rule; plus global canonical rules for filters and catalogs
|
||||||
|
|
||||||
|
**Q: Global canonical rules model**
|
||||||
|
|
||||||
|
Options presented: Parameter list; Pattern -> canonical rules; Both
|
||||||
|
|
||||||
|
**User's choice:** Query parameter list: parameters marked as content (e.g. page) stay in the canonical, filter/sort parameters are dropped; refinable per path pattern (e.g. /katalog/*)
|
||||||
|
|
||||||
|
**Q: Meta robots granularity**
|
||||||
|
|
||||||
|
Options presented: Index/follow + advanced; Index/follow only
|
||||||
|
|
||||||
|
**User's choice:** index/noindex and follow/nofollow with 'inherit' as default, plus advanced: max-snippet, max-image-preview, max-video-preview, noarchive, nosnippet, noimageindex, unavailable_after; same directives also as X-Robots-Tag header (works for PDFs and files)
|
||||||
|
|
||||||
|
**Q: Editor aids in the SEO tab**
|
||||||
|
|
||||||
|
Options presented: SERP preview; Counters and warnings; Social card preview; Focus keyphrase and analysis
|
||||||
|
|
||||||
|
**User's choice:** Live Google SERP preview (desktop/mobile, pixel truncation); character and pixel counters with warnings (too long/short, empty, duplicate title/description); focus keyphrase with Yoast-style checks (phrase in title, H1, description, URL; text length). Social card preview not wanted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MCP management
|
||||||
|
|
||||||
|
**Q: How MCP tools address a page**
|
||||||
|
|
||||||
|
Options presented: By URL, server resolves owner; Always URL rule; By record ID
|
||||||
|
|
||||||
|
**User's choice:** By URL from the audit; the server resolves the owner: a record behind the URL gets the change in its SEO tab, otherwise a URL rule is created; the answer says where it was written; a URL rule can be forced
|
||||||
|
|
||||||
|
**Q: Bulk operations and safety**
|
||||||
|
|
||||||
|
Options presented: Batch + dry run; Batch without preview; Single calls
|
||||||
|
|
||||||
|
**User's choice:** Batch tools taking a list of changes, with dry_run returning the diff and conflicts (redirect loops/chains, noindex on a sitemap page, canonical to a redirected page); apply is all-or-nothing
|
||||||
|
|
||||||
|
**Q: Read and audit tools**
|
||||||
|
|
||||||
|
Options presented: Effective SEO of a URL; Site issue report; CSV export/import; History and undo
|
||||||
|
|
||||||
|
**User's choice:** All four: effective SEO of a URL with the source of every value; site issue report; CSV export/import with preview (also in the admin); change history with author/time/before/after and undo of one change or a whole batch
|
||||||
|
|
||||||
|
**Q: SEO permissions**
|
||||||
|
|
||||||
|
Options presented: Separate per area; One SEO permission
|
||||||
|
|
||||||
|
**User's choice:** One SEO permission for everything (not split per area)
|
||||||
|
|
||||||
|
**Q: What the issue report is based on**
|
||||||
|
|
||||||
|
Options presented: CMS data + crawler; CMS data only; Crawler in a separate phase
|
||||||
|
|
||||||
|
**User's choice:** CMS data checks plus a built-in crawler (on demand or scheduled) fetching rendered pages from the sitemap and links: status codes, H1/H2/H3, canonical, robots, JSON-LD, internal links, orphan pages; results in the admin and over MCP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Redirects
|
||||||
|
|
||||||
|
**Q: Status codes**
|
||||||
|
|
||||||
|
Options presented: 301,302,307,308+410; 301 and 302 only; 301,302+410
|
||||||
|
|
||||||
|
**User's choice:** 301, 302 and 410 Gone (no 307/308)
|
||||||
|
|
||||||
|
**Q: Source matching**
|
||||||
|
|
||||||
|
Options presented: Exact + patterns + regex; Exact + patterns; Exact only
|
||||||
|
|
||||||
|
**User's choice:** Exact (default), wildcard patterns (/old/* -> /new/*) and regex with capture groups; options: ignore case, trailing slash, query string (ignore or pass through); evaluation order exact, then patterns, then regex
|
||||||
|
|
||||||
|
**Q: Automatic redirects**
|
||||||
|
|
||||||
|
Options presented: Slug change -> 301; Deletion -> choice; Loop protection
|
||||||
|
|
||||||
|
**User's choice:** Slug/path change of a record creates a 301 from the old URL and flattens existing chains. Not chosen: asking on deletion
|
||||||
|
|
||||||
|
**Q: Loops and chains**
|
||||||
|
|
||||||
|
Options presented: Block loops, warn on chains; Block loops and chains; Report only
|
||||||
|
|
||||||
|
**User's choice:** Loops always refused (admin, MCP, import); chains saved with a warning and listed in the issue report
|
||||||
|
|
||||||
|
**Q: Deleted or unpublished record without a choice**
|
||||||
|
|
||||||
|
Options presented: 404 + 404 log; Automatic 410; Automatic 301 to parent
|
||||||
|
|
||||||
|
**User's choice:** Plain 404, and the URL shows up in the 404 log; no automatic 410 or 301
|
||||||
|
|
||||||
|
**Q: Monitoring**
|
||||||
|
|
||||||
|
Options presented: 404 log; Rule hit counter; Validity dates
|
||||||
|
|
||||||
|
**User's choice:** 404 log (hits, last hit, referer) with one-click/MCP redirect creation. Not chosen: per-rule hit counters, validity dates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. robots.txt and noindex policy
|
||||||
|
|
||||||
|
**Q: robots.txt editing**
|
||||||
|
|
||||||
|
Options presented: Editor + auto parts + staging lock; Plain editor; Structured rules form
|
||||||
|
|
||||||
|
**User's choice:** Text editor with syntax validation and a tester ('is this URL blocked for Googlebot?'); the Sitemap: line is appended automatically; every non-production environment always serves Disallow: / and X-Robots-Tag: noindex regardless of the content
|
||||||
|
|
||||||
|
**Q: Default robots per page kind**
|
||||||
|
|
||||||
|
Options presented: Search results; Filters and sorting; Tags and authors; Technical pages
|
||||||
|
|
||||||
|
**User's choice:** noindex: login, register, password reset, all signed-in user pages, internal search results (noindex,follow), filter/sort parameter URLs (noindex,follow), technical pages (cart/checkout later, draft previews, thank-you pages, 404/errors). Tags: noindex,follow by default. Authors: always index,follow. Pagination (e.g. blog page 2+): always index,follow with a self-referencing canonical.
|
||||||
|
|
||||||
|
**Q: How the frontend knows a page is private**
|
||||||
|
|
||||||
|
Options presented: Auto from auth + admin patterns; Admin patterns only; By hand in code
|
||||||
|
|
||||||
|
**User's choice:** Automatic for every route that requires sign-in (noindex,nofollow + X-Robots-Tag) plus path patterns in the admin for public but non-indexable pages; the user plugin's auth layer marks its own pages
|
||||||
|
|
||||||
|
**Q: Consistency of noindex, robots.txt and sitemaps**
|
||||||
|
|
||||||
|
Options presented: Automatic; Warnings only
|
||||||
|
|
||||||
|
**User's choice:** Automatic: pages with noindex, a canonical elsewhere or a redirect drop out of the XML and HTML sitemaps; warnings when a noindex page is blocked in robots.txt and when a sitemap URL is blocked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sitemaps (XML boost, HTML sitemap)
|
||||||
|
|
||||||
|
**Q: User's sitemap idea**
|
||||||
|
|
||||||
|
Options presented: free text after 6 proposals
|
||||||
|
|
||||||
|
**User's choice:** Sitemap index split by type: categories, tags, posts, CMS pages, and offer/service pages separately; each type batched in files of 1000 URLs so ~20 sitemaps can be added to GSC and monitored one by one; no priority and no changefreq. Also accepted: real lastmod from the actual content change, image sitemap (featured image + images in the body), Google News sitemap, IndexNow, HTML sitemap generated from the same source
|
||||||
|
|
||||||
|
**Q: Existing sm-sitemap-plugin (PHP byte parity, priority tiers, single file)**
|
||||||
|
|
||||||
|
Options presented: Extend, PHP mode as legacy switch; New mode replaces old; New SEO plugin, old untouched
|
||||||
|
|
||||||
|
**User's choice:** New mode replaces the old one: PHP parity removed, every site moves to the new format including grzybyfunkcjonalne. BREAKING change on a shared core plugin, explicitly chosen by the user: new plugin version, grzyby migrated in the same phase, parity goldens retired
|
||||||
|
|
||||||
|
**Q: Where CMS and offer/service pages come from (Nuxt pages the backend does not know)**
|
||||||
|
|
||||||
|
Options presented: Page registry in CMS; Pages plugin in a later phase; Manual sitemap URLs only
|
||||||
|
|
||||||
|
**User's choice:** A page registry in the CMS (admin + MCP): frontend URL, page type (page, offer, service, landing), SEO tab, modification date; Nuxt pages read their SEO from it; sitemaps group by type; the frontend can report its routes at build time so none are missed
|
||||||
|
|
||||||
|
**Q: Batch size**
|
||||||
|
|
||||||
|
Options presented: 1000, configurable; Fixed 1000
|
||||||
|
|
||||||
|
**User's choice:** 1000 by default, configurable in settings (max 50 000); stable file numbering (sitemap-posts-1.xml ...); oldest first so new URLs land in the last file and earlier files do not shift
|
||||||
|
|
||||||
|
**Q: Google News sitemap and IndexNow**
|
||||||
|
|
||||||
|
Options presented: Both optional; IndexNow always, News optional; No News
|
||||||
|
|
||||||
|
**User's choice:** Both optional, enabled in settings; News for chosen categories/types; IndexNow key generated in the admin, pinged on publish, slug change and deletion
|
||||||
|
|
||||||
|
**Q: HTML sitemap content**
|
||||||
|
|
||||||
|
Options presented: All indexable, groups and paging; Structure only
|
||||||
|
|
||||||
|
**User's choice:** Everything indexable: no 404s, no redirected URLs, no noindex (same set as the XML sitemaps); grouping and paging at Claude's discretion
|
||||||
|
|
||||||
|
**Q: HTML sitemap location and linking**
|
||||||
|
|
||||||
|
Options presented: Footer on every page; Home page only
|
||||||
|
|
||||||
|
**User's choice:** Linked from the footer of every page (so also the home page); path set in the admin, default /mapa-strony (pl) or /sitemap (en) with locale variants; page component in a Nuxt layer, overridable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Pagination
|
||||||
|
|
||||||
|
**Q: Robots and canonical on page 2+ (from topic 4)**
|
||||||
|
|
||||||
|
Options presented: stated by user in topic 4
|
||||||
|
|
||||||
|
**User's choice:** Always index,follow with a self-referencing canonical
|
||||||
|
|
||||||
|
**Q: URL form**
|
||||||
|
|
||||||
|
Options presented: Per site, default ?page=N; Always /strona/N; Always ?page=N
|
||||||
|
|
||||||
|
**User's choice:** Per site, default ?page=N; a site may choose a path form (/blog/strona/2), e.g. to keep WinterCMS URLs; the other form 301s to the configured one
|
||||||
|
|
||||||
|
**Q: Title, description and content on page 2+**
|
||||||
|
|
||||||
|
Options presented: Title suffix; Description suffix; Category text on page 1 only; H1 with page number
|
||||||
|
|
||||||
|
**User's choice:** All four: title suffix ('Blog - strona 2 z 14 | Site'), description suffix ('Strona 2 z 14'), category long text (pillar content) only on page 1, H1 with the page number on 2+
|
||||||
|
|
||||||
|
**Q: Mechanism**
|
||||||
|
|
||||||
|
Options presented: <a> links + optional load more; Numbered links only
|
||||||
|
|
||||||
|
**User's choice:** Always numbered server-rendered <a href> links; 'load more'/infinite scroll only as an optional enhancement over them that updates the URL to ?page=N; <link rel=prev/next> in head
|
||||||
|
|
||||||
|
**Q: Invalid page numbers**
|
||||||
|
|
||||||
|
Options presented: page=1 -> 301, out of range -> 404; Out of range -> 301 to last
|
||||||
|
|
||||||
|
**User's choice:** ?page=1 (or /strona/1) 301 to the unnumbered URL; beyond the last page 404; garbage (?page=abc, -1) 404
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Structured data per page type
|
||||||
|
|
||||||
|
**Q: Default mapping**
|
||||||
|
|
||||||
|
Options presented: As in the table; With changes
|
||||||
|
|
||||||
|
**User's choice:** JSON-LD as one @graph linked by @id. Every page: WebPage + BreadcrumbList + refs to WebSite and Organization. Home: WebSite (+SearchAction when search exists) + Organization/LocalBusiness from global settings. Post: BlogPosting (or Article/NewsArticle) with author Person, publisher Organization, image, dates, wordCount, articleSection. Category/tag: CollectionPage + ItemList. Author: ProfilePage + Person (sameAs). Service/offer: Service (provider, areaServed, optional Offer with price). Contact: ContactPage + LocalBusiness/Organization with address and hours. FAQ plugin page: FAQPage. Post with YouTube: VideoObject. Generated automatically, no editor work.
|
||||||
|
|
||||||
|
**Q: Editor control per record/page**
|
||||||
|
|
||||||
|
Options presented: Type + fields + custom JSON-LD; Type + fields; Automatic only
|
||||||
|
|
||||||
|
**User's choice:** Type choice in the SEO tab (e.g. BlogPosting/NewsArticle/Article; Service/Product), a type-dependent field form, plus a 'custom JSON-LD' field for advanced users, validated on save and merged into the graph
|
||||||
|
|
||||||
|
**Q: Publisher in global settings**
|
||||||
|
|
||||||
|
Options presented: Organization/LocalBusiness/Person; Organization only
|
||||||
|
|
||||||
|
**User's choice:** Choice of Organization, LocalBusiness with subtype (address, geo, opening hours, phone) or Person for personal blogs; logo, sameAs (social, Wikipedia, KRS), contact data
|
||||||
|
|
||||||
|
**Q: Validation**
|
||||||
|
|
||||||
|
Options presented: Admin + report; Google test link only
|
||||||
|
|
||||||
|
**User's choice:** In the admin against Google rich-result required/recommended properties per type, plus crawler checks of rendered JSON-LD, plus a 'check in Rich Results Test' link per URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Heading structure
|
||||||
|
|
||||||
|
**Q: H1 inside body content (page title is the H1)**
|
||||||
|
|
||||||
|
Options presented: No H1 in editor + automatic conversion; Refuse save; Warning only
|
||||||
|
|
||||||
|
**User's choice:** Editor toolbar offers H2-H4 only; any H1 in content (pasted, imported from WinterCMS, written via MCP) is converted to H2 on save with a notice to the editor
|
||||||
|
|
||||||
|
**Q: Skipped levels and empty headings**
|
||||||
|
|
||||||
|
Options presented: Editor warning + report; Automatic renumbering on save
|
||||||
|
|
||||||
|
**User's choice:** Live heading outline in the editor flags errors (skipped level, empty heading, heading that is only a link); the crawler checks rendered pages (exactly one H1, no skipped levels, H1 consistent with the title); no silent rewriting beyond the H1 conversion
|
||||||
|
|
||||||
|
**Q: Headings in template elements**
|
||||||
|
|
||||||
|
Options presented: Rule in Nuxt layers; Leave as is
|
||||||
|
|
||||||
|
**User's choice:** One convention enforced in the Nuxt layers and their tests: H1 only in the main content; post cards on listings are H2; sidebar and footer widgets do not use content headings (styled text) so they do not disturb the outline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. hreflang basics
|
||||||
|
|
||||||
|
**Q: Where hreflang is emitted**
|
||||||
|
|
||||||
|
Options presented: Head and sitemap; Sitemap only
|
||||||
|
|
||||||
|
**User's choice:** Both <link rel=alternate hreflang> in every page head and xhtml:link in the XML sitemaps, from one source (record locale alternates); plus <html lang>, og:locale and og:locale:alternate
|
||||||
|
|
||||||
|
**Q: x-default**
|
||||||
|
|
||||||
|
Options presented: Default-locale version; Configurable
|
||||||
|
|
||||||
|
**User's choice:** Points to the default-locale version
|
||||||
|
|
||||||
|
**Q: Page without a translation**
|
||||||
|
|
||||||
|
Options presented: No hreflang for that locale; Fallback to default locale
|
||||||
|
|
||||||
|
**User's choice:** No hreflang for that locale; never serve default-locale content under another locale's URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. SSR guarantee
|
||||||
|
|
||||||
|
**Q: How public pages are guaranteed server-rendered**
|
||||||
|
|
||||||
|
Options presented: Hard rule + checks; Documentation guideline only
|
||||||
|
|
||||||
|
**User's choice:** Hard rule: indexable pages are always SSR or SSG; ssr:false only on noindex routes; the Nuxt SEO layer fails the build when an indexable route has ssr:false; the crawler compares raw (no-JS) HTML with rendered HTML and reports content or links that appear only after JS
|
||||||
|
|
||||||
|
**Q: Server guarantees to search engines**
|
||||||
|
|
||||||
|
Options presented: Real HTTP codes; Links as <a href>; Content without interaction; SEO-friendly images
|
||||||
|
|
||||||
|
**User's choice:** All four: real HTTP status codes (404 not soft-404 200, 410 and 301 per rules); all navigation (menus, pagination, cards, breadcrumbs) as server-rendered <a href>; content in tabs/accordions/FAQ present in the HTML, only hidden by style; images with native loading=lazy and real src/alt, dimensions, featured image not lazy with fetchpriority
|
||||||
|
|
||||||
|
**Q: sm-bm-app (client-only SPA)**
|
||||||
|
|
||||||
|
Options presented: Separate phase if indexable; Internal app; In this phase
|
||||||
|
|
||||||
|
**User's choice:** Internal application with no public indexable pages: it only gets noindex and Disallow, no SSR migration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Internal linking (added by user)
|
||||||
|
|
||||||
|
**Q: User requirement**
|
||||||
|
|
||||||
|
Options presented: free text
|
||||||
|
|
||||||
|
**User's choice:** Avoid generic anchors ('kliknij tutaj'); post links are plain followed links (no rel attribute); smart default internal linking consistent with SEO. User first proposed image links with rel=nofollow; after the note that internal nofollow is only a hint and does not conserve PageRank, chose one link per card
|
||||||
|
|
||||||
|
**Q: Post image in cards (listings, related)**
|
||||||
|
|
||||||
|
Options presented: Link with rel=nofollow; One link per card; Configurable per site
|
||||||
|
|
||||||
|
**User's choice:** One link per card: only the title is a text link; the image is clickable for users but has no separate <a> in the HTML (or tabindex=-1, aria-hidden), so Googlebot sees one link with a good anchor
|
||||||
|
|
||||||
|
**Q: Anchors**
|
||||||
|
|
||||||
|
Options presented: Layers + content validation; Layers only
|
||||||
|
|
||||||
|
**User's choice:** Layer components never use generic anchors (card link = post title; any 'read more' carries the title, e.g. 'Czytaj: <title>'); the content editor warns on links whose anchor is on an editable generic list (pl and en); the crawler reports them
|
||||||
|
|
||||||
|
**Q: Smart internal linking**
|
||||||
|
|
||||||
|
Options presented: Related posts; Phrase auto-linking; Editor suggestions; Orphan protection
|
||||||
|
|
||||||
|
**User's choice:** All four: related posts (3-6, from tags, category and optionally the focus keyphrase, server-rendered) plus previous/next post in category; phrase auto-linking table phrase -> URL (first occurrence, max N links per post, never to itself, never inside headings; focus keyphrases can feed the table); link suggestions in the editor with inbound/outbound link counters; orphan report with suggestions where to link from
|
||||||
|
|
||||||
|
**Q: Outbound links in content**
|
||||||
|
|
||||||
|
Options presented: No attribute + per-link choice; Always nofollow
|
||||||
|
|
||||||
|
**User's choice:** Plain followed link by default; per-link rel checkboxes in the editor (nofollow, sponsored, ugc); a domain list that always gets nofollow or sponsored; target=_blank with rel=noopener for external links
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. URL normalisation, AI bots, media, breadcrumbs (added)
|
||||||
|
|
||||||
|
**Q: URL normalisation**
|
||||||
|
|
||||||
|
Options presented: Per-site settings; Fixed defaults
|
||||||
|
|
||||||
|
**User's choice:** Per-site settings: https always; www or not; trailing slash or not; lowercase path; double slashes, /index.html and similar to the clean form; one hop to the final form instead of a chain
|
||||||
|
|
||||||
|
**Q: AI bots in robots.txt**
|
||||||
|
|
||||||
|
Options presented: Per-bot switches, allow by default; Block training by default; No switches
|
||||||
|
|
||||||
|
**User's choice:** Per-bot switches (GPTBot, OAI-SearchBot, ClaudeBot, Google-Extended, PerplexityBot, CCBot, Applebot-Extended, others), distinguishing AI search bots from training bots; all allowed by default; optional llms.txt generated from the sitemap
|
||||||
|
|
||||||
|
**Q: Image alt and media**
|
||||||
|
|
||||||
|
Options presented: Alt required; SEO file names; Formats and sizes; Alt via MCP and report
|
||||||
|
|
||||||
|
**User's choice:** All four: alt required in the media manager and when inserting into content (warning when missing; 'decorative' checkbox gives alt=''); SEO file names (slug on upload, editable); automatic WebP/AVIF and srcset widths keeping the original; missing-alt report (CMS + crawler) and an MCP tool for bulk alt filling
|
||||||
|
|
||||||
|
**Q: Breadcrumbs**
|
||||||
|
|
||||||
|
Options presented: Layer component, one source; JSON-LD only
|
||||||
|
|
||||||
|
**User's choice:** Visible breadcrumbs component in a Nuxt layer as <a> links, from the same source as BreadcrumbList JSON-LD; on by default on every page except home, can be disabled or restyled; a post's trail goes through its primary category, selectable in the SEO tab
|
||||||
|
|
||||||
|
**Q: Strict URLs (raised by user: WinterCMS answered 200 for /<any-category>/<post>)**
|
||||||
|
|
||||||
|
Options presented: 301 to the correct URL; 404
|
||||||
|
|
||||||
|
**User's choice:** Every segment must match; a mismatching URL of an existing record 301s to its canonical URL; a wrong URL never answers 200
|
||||||
|
|
||||||
|
**Q: Phase split**
|
||||||
|
|
||||||
|
Options presented: Several consecutive phases; One phase, many plans
|
||||||
|
|
||||||
|
**User's choice:** Dawid: not his decision but Jakub's; recorded as open question Q-01 with Claude's recommendation (three phases 32/33/34)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude's Discretion
|
||||||
|
|
||||||
|
- Where redirects execute: rules live in the CMS; the server of the page enforces them (Nuxt Nitro middleware in a shared layer for frontend pages, Go for its own paths and statically embedded sites)
|
||||||
|
- HTML sitemap grouping (pages, offer, category tree, posts, indexable tags) and paging of large groups (~100-200 links per page)
|
||||||
|
- Fix the Journal API gap (findPostBySlugOrID matches only the default-locale slug) so translated slugs resolve; required for hreflang
|
||||||
|
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
- Social card preview; per-redirect hit counters and validity dates; 307/308; e-commerce structured data
|
||||||
|
- Todo sitemap-xslt-browser-deprecation stays a separate backlog task (user decision)
|
||||||
Reference in New Issue
Block a user