app/utils/bodyLinks.ts, pure and run by content layers at SSR on
sanitized body HTML (UI-SPEC L7 steps 2 to 4):
- autolink(html, links, max, self): first occurrence of each CMS phrase,
whole words, case-insensitive, at most max per text, never inside a,
headings, code, pre, figcaption or button, never to the page itself;
only site paths and http(s) URLs, escaped, as a plain <a href>
- applyOutbound(html, {siteHost, newTab, nofollowDomains,
sponsoredDomains, hint}): editor rel kept, nofollow/sponsored from the
domain lists, target=_blank, rel noopener and the visually hidden hint
only while new tab is on; internal links untouched
- imageAttrs(html): loading=lazy and decoding=async, stored width,
height and alt="" kept
- the module registers seo-sitemap-<locale> at seo.htmlSitemap.paths
(defaults pl /mapa-strony, en /sitemap) and seo-sitemap-group-<locale>
at <path>/:group (plus <path>/:group/<segment>/:page in the path form);
a path an app page already has stays the app's
- SeoHtmlSitemapPage (global, overridable): H1, an H2 per non-empty
group, categories nested, a large group's link to its group page,
sitemap.empty; group pages with a built trail, page suffix and
<SeoPagination>; 404 for an unknown group or a page past the last,
301 for ?page=1, 503 when the sitemap API fails
- GET /api/__seo__/sitemap proxies the sitemap plugin's HTML API (only
while the CMS has the HTML sitemap on for the locale)
- <SeoSitemapLink>: the footer link while the HTML sitemap is on
- L3 default styles
- <SeoPagination> (UI-SPEC L2): previous and next (left out at the
ends), page 1, the last page and five pages around the current one with
gap items; nothing when last <= 1; props current, last, labels,
classPrefix, locale
- usePagination(): hrefs in the CMS form (resolve answer, then site
answer, then seo.pagination), other query parameters kept, page 1
unnumbered, <link rel=prev/next> in the head
- the CMS head fills %pages% in the title and description once the
listing's page count is known (seoFillPages)
- seoPaginationWindow, seoPageHref, seoPageNumber; L2 default styles
- <SeoBreadcrumbs> (UI-SPEC L1): the resolve answer's trail as nav > ol
with server-rendered links and the last item as aria-current text;
nothing with fewer than two items, with page meta seo.breadcrumbs false
or with the option seo.breadcrumbs false; props items, label,
hideCurrent, locale
- one trail per page: the first rendered instance claims it
(seoClaimBreadcrumbs, kept per path)
- seoSitePath: same-origin CMS URLs become paths, non-http(s) URLs are
never links; useSeoLabels takes an optional locale
- L1 default styles with CSS-generated separators
- useSeoLabels(): the L8 dictionary (en, pl) of the page's content locale
(resolve locale, then html_lang, then en), overridable and extendable in
app.config.ts under seo.labels; no i18n dependency
- seo-tokens.css (spacing, colour, small type, breadcrumb separator) and
seo-base.css (.seo-visually-hidden, the focus ring of layer links), loaded
before the app's CSS and styling only .seo-* classes
- Nitro middleware resolves page requests once and hands the answer to the
render (event.context.seo); the route middleware reuses it
- useCmsSeo: html lang, hreflang links, og:locale and exactly one JSON-LD
script with one @graph (CMS nodes plus useSchemaNode nodes, merged by @id)
- local module (configKey seo): cms (alias of public.seo.enabled),
breadcrumbs, links.newTabHint, rootFiles, resolveFiles
- GET /api/__seo__/resolve for client navigation through the Nuxt server
- smoke harness: count=, exact header=, noheader=, reqheader=, repeated
options, resolves/apicalls/noreport lines, --no-site; v0.1.1 suite runs
unchanged against a backend without /site