Commit Graph

26 Commits

Author SHA1 Message Date
Jakub Zych
3868f3d74d fix: root files follow the last site answer, a 404 included, while the backend is down
An SEO-less site's robots.txt, sitemap.xml and llms.txt keep reaching
the app during an outage longer than the cache TTL; a backend that never
answered still gets the 503.

quick-261104-xae, review WR-04.
v0.2.0
2026-11-05 00:23:19 +01:00
Jakub Zych
973142a714 chore: version 0.2.0 and install pin
- package.json version 0.2.0; the README install line already pins #v0.2.0
2026-11-04 22:54:59 +01:00
Jakub Zych
8cdf35929a fix: root files answer 503 while the backend has not answered
- rootFileTargets gives robots.txt, sitemap.xml and llms.txt their backend
  target also in state unknown without a last answer, so the failing fetch
  answers 503 with Retry-After: 60 instead of the app's 404 (L4 WR-08)
- an older backend (absent) still passes through; sitemap files and
  verification files still need a site answer
- expect-seo-down.txt expects the 503s and states the staging reason
2026-11-04 22:49:40 +01:00
Jakub Zych
a651de4675 fix: HTML sitemap pages once per locale, never over a catch-all by default
- seoSitemapPages (lib/sitemapPages.ts) builds the routes per locale; the
  module adds them from a pages:resolved hook registered on ready, after
  @nuxtjs/i18n has localised the app's routes, so no cross-locale copies
  exist and each name ends in the locale's i18n suffix (L4 WR-05)
- a path an app catch-all covers is left to it and logged at build unless
  seo.htmlSitemap.overCatchAll is true (L4 WR-04)
- SitemapLink falls back to the i18n locale and renders only when the path
  resolves to the layer's sitemap page of that locale
- an app with only app.vue no longer gets its router turned on by the pages
2026-11-04 22:47:58 +01:00
Jakub Zych
7588aa53b0 fix: HTML sitemap page 1 redirects with the locale's segment
- seoFirstPageRedirect answers the unnumbered URL or null when the target
  is the URL itself, so /mapa-strony/<group>/strona/1 no longer loops (L4 WR-01)
- SeoHtmlSitemapPage takes the segment from the site answer, else the build
  option, else the param, and redirects only when the helper answers a target
2026-11-04 22:41:18 +01:00
Jakub Zych
7a26692001 feat: the route report sends the noindex static paths (tracer)
- noindexStaticRoutes: requiresAuth, noindex/none seo.robots and static
  pages under seo.noindexRoutes, i18n copies included, never patterns or
  the layer's HTML sitemap pages (G1, gap-2 contract)
- #seo-routes exports noindexPaths; routeReportBody(paths, noindex) answers
  {routes, noindex} with the same limits and keeps the JSON body within the
  older plugin's 4,113,024 bytes, dropping noindex entries first
- smoke grammar noindex= and lacksnoindex=; README carries the contract
2026-11-04 22:40:38 +01:00
Jakub Zych
b3a0078fb0 fix: leave the i18n copies of a page in other locales out of the route report
With prefix_except_default (or prefix_and_default) @nuxtjs/i18n adds a copy
of every page per other locale (/pl/contact next to /contact). The route
report sent them as pages of their own, so the CMS registry got a second,
untranslated page for each. They are the page's translations: the report
now skips routes named with another locale's suffix (contact___pl).
2026-11-04 20:13:05 +01:00
Jakub Zych
75cc100336 test: usePagination hrefs, head links and page count; drift pattern forms
- the query and path forms from the resolve answer, the site answer and
  the layer option, page 1 unnumbered
- prev and next links at the ends, %pages% from the page count
- routeServes reads {param} and :param alike and ignores the query part
2026-11-04 19:47:18 +01:00
Jakub Zych
7af91fc5e4 docs: body link utilities and the app-without-pages note
- autolink, applyOutbound and imageAttrs: the body link rules a content
  layer runs at SSR on sanitized HTML (UI-SPEC L7)
- htmlSitemap: an app without pages sets false, since the sitemap pages
  would turn the router on
- the purge route's copies in the journal, menu and FAQ layers are gone
2026-11-04 14:08:22 +01:00
Jakub Zych
db44ca4d72 feat: body link utilities (auto-links, outbound rules, image attributes)
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
2026-11-04 13:40:47 +01:00
Jakub Zych
b051bda080 docs: build guard, noindex routes and components
- README: build guard, noindex routes and page meta keys, route report,
  drift check, the new module options, the Components section
  (SeoBreadcrumbs, SeoPagination, usePagination, the HTML sitemap pages
  and their override, SeoSitemapLink), useSeoSite and the new helpers,
  guard:check and the new smoke grammar
2026-11-04 12:02:15 +01:00
Jakub Zych
aee6957bd6 feat: HTML sitemap pages and footer link
- 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
2026-11-04 12:00:00 +01:00
Jakub Zych
f2d13d6264 feat: pagination component and composable
- <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
2026-11-04 11:59:51 +01:00
Jakub Zych
ebb0c8084e feat: breadcrumbs component
- <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
2026-11-04 11:59:17 +01:00
Jakub Zych
3abc986f42 feat: route report and drift check at server start
- the module writes #seo-routes (static paths and route patterns) from the
  resolved pages; options seo.routeReport, seo.pagination and
  seo.htmlSitemap reach the public runtime config
- seo-route-report: POST /registry/routes once per process with the
  route-report token, one retry after 60 s on failure, nothing without a
  token, with the layer off or with routeReport false
- seo-drift: one "[vue-seo-layer] drift:" warning per mismatch between
  the CMS's site answer (URL patterns, pagination, HTML sitemap paths)
  and the build
- GET /api/__seo__/site and useSeoSite()
- smoke grammar routereport and log, fixture registry route, drift run
2026-11-04 11:49:21 +01:00
Jakub Zych
9cc2cda924 feat: noindex for declared and signed-in routes
- app plugin seo-route-robots: a page whose meta sets requiresAuth: true
  gets robots noindex, nofollow, a page with seo.robots gets its value,
  a path under seo.noindexRoutes gets noindex, nofollow; meta tag above
  the CMS head entry and X-Robots-Tag during SSR, never on staging
- requiresAuth and seo added to Nuxt's page meta extraction keys
- playground /konto, /szkic and /prywatne with their smoke lines
2026-11-04 11:49:14 +01:00
Jakub Zych
b4680af0e3 feat: the build fails when an indexable route renders only in the browser (tracer)
- lib/guard.ts: pure checks (isNoindex with Nitro route-rule patterns,
  findOffenders over the app's ssr flag, every merged route rule,
  .client.vue pages and experimental.inlineRouteRules)
- module options seo.noindexRoutes and seo.indexable; declared noindex
  routes get X-Robots-Tag: noindex, nofollow through their route rule
- a production build fails with the D-70 message; nuxt dev and
  nuxt prepare only warn
- testing/guard-check.mjs builds four fixture apps (guard:check)
2026-11-04 11:42:08 +01:00
Jakub Zych
6636cde70a fix: root files fall through until the backend has given a site answer
With the SEO backend unreachable since start, /robots.txt and /sitemap.xml
answered 503 where v0.1.1 left them to the app (a consumer's smoke expects
the app's 404). The root proxies now need a site answer; the last one seen
is kept while the backend is down, so a failure after it is still 503.
2026-11-04 09:31:39 +01:00
Jakub Zych
c2357cd7f9 docs: README
- options (cms alias of public.seo.enabled, breadcrumbs, links.newTabHint,
  rootFiles, resolveFiles), the new runtime keys and the moved purge keys
- the resolve and site answer contract, one JSON-LD graph, root files,
  the purge route, 404 report, labels and tokens, the smoke runs
2026-11-04 09:29:02 +01:00
Jakub Zych
aa7480e003 feat: SEO labels, tokens and base styles
- 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
2026-11-04 09:27:57 +01:00
Jakub Zych
d1629439c7 feat: root files from the CMS and the 404 report
- server/middleware/seo-root.ts proxies /robots.txt, /sitemap.xml,
  /sitemap-<name>.xml, /llms.txt and the verification and IndexNow key
  files the site answer lists in root_files, with the backend's status,
  content type and X-Robots-Tag; a backend failure is 503 Retry-After 60
- server/lib/site.ts caches GET /_seo/api/v1/site (300 s, cleared by the
  purge); a backend without it (404) keeps the v0.1.1 behaviour: no root
  proxies, the verification lookup by name
- the 404 report and the verification lookup honour seo.cms and rootFiles
- smoke runs for the layer off (seo.cms and the v0.1.1 enabled switch), the
  CMS down (fail open) and no 404 token; npm run purge:check
2026-11-04 09:26:00 +01:00
Jakub Zych
691fa09b0f feat: shared site cache purge route clears the SEO caches
- POST /api/__site__/cache/purge, its library and the site config plugin
  moved from vue-journal-layer with the same contract (token rules, 404 for
  proxied requests unless allowProxied, deprecated siteCache names); the
  route also clears the resolve cache
- server/plugins/seo-cache-purge.ts clears the SEO caches after any 200
  purge, so an app whose journal, menu or FAQ layer copy of the route wins
  keeps fresh SEO answers until those copies are removed
- cachePurge and siteCache runtime keys; /api/__site__/** never cached
- testing/purge-check.mjs (CMS answer change, plus a redirect added for a
  route-cached page applying at once) and purge-token-check.mjs
2026-11-04 09:23:15 +01:00
Jakub Zych
e3baa7028c feat: redirect and 410 enforcement, X-Robots-Tag and the resolve cache
- the Nitro middleware answers redirect (301/302, Location from the answer
  only, same-site targets as paths as in v0.1.1) and gone (410) before the
  render and the route cache, sets X-Robots-Tag, and answers 404 for a
  pagination.invalid answer; other not_found answers stay with Nuxt
- resolve answers cached in an LRU (seo.resolveCacheTtl, default 60 s,
  1000 entries) with in-flight de-duplication; a clear detaches loads in
  flight so no answer fetched before a purge is stored after it
- the playground route-caches /blog/** (swr 300)
2026-11-04 09:20:20 +01:00
Jakub Zych
160b53b2f5 feat: CMS head and one JSON-LD graph on every server-rendered page (tracer)
- 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
2026-11-04 09:15:26 +01:00
Jakub Zych
7d52c5d2e5 fix: canonical and social URLs answered as paths become absolute on the request origin v0.1.1 2026-10-31 13:53:30 +01:00
Jakub Zych
ac59f3c80d feat: vue-seo-layer, the Nuxt half of the SummerCMS SEO plugin v0.1.0 2026-10-31 13:45:37 +01:00