Amends 0165 with decisions 6
and 7: code that client route code calls, such as
alternates and localePath, stays out of
.server modules, and a route’s metadata follows the SEO chain of decision 7. The note on 0165
records the change.Context
Public pages need the metadata that search engines and crawlers read: sitemaps, robots rules, structured data, canonical URLs, hreflang alternates and the locale path of each page. Two problems came up. Some of this code sat in.server modules, but
route code that runs in the browser also needs the alternates and the locale path, so those modules reached the client bundle.
And structured data written into a page without escaping can close its script element. The metadata of a route also comes from
several places, so it needs one defined order. 0127 lists an seo
package in the cross-cutting category as planned; this ADR decides what it holds.
Decision
- One package,
@stackra/seo, in the cross-cutting category. It owns sitemaps, robots rules, the structured-data escaper, canonical redirects, alternates and locale paths. - Sitemaps are paged. A site publishes a sitemap index that points to pages of URLs. Each page stays within the sitemap protocol limits of 50,000 URLs and 50 MB uncompressed.
- Robots rules are groups, one per crawler. A rule written for one crawler does not apply to another.
- Structured data goes through the escaper. JSON-LD is serialised so that no value can close the script element or start markup. A page does not write structured data raw.
- Canonical redirects depend on the URL alone.
ctx.redirectToCanonicalanswers with a permanent redirect to the page’s canonical URL. It never depends on the visitor, as 0297 requires for locale paths. - Alternates and locale paths are ordinary functions.
alternatesandlocalePathmove out of.servermodules into@stackra/seo, so client route code can call them. The route adapter takes the clients it needs from the load context. - One SEO chain per route. Metadata is built in this order: the app’s defaults, the route’s static
seo, the route’smeta()method, then the tenant’s SEO record (0116). The canonical URL, alternates, site name and Open Graph image are derived, so a route passes only the fields that differ.
Consequences
- A route author writes only what differs from the defaults.
- Structured data cannot break a page, and robots rules and sitemaps are generated from one place, not written by hand.
- A route that needs a locale path imports it like any other function, from client or server code.
- A large site has a sitemap index and several sitemap pages, not one file.
Enforcement
planned — tests in@stackra/seo for the escaper (values that try to close the script element or add markup), the sitemap paging
at the protocol limits, the robots groups, the redirect status and target, and the order of the SEO chain. Linear: STA-341.
planned — a no-restricted-imports pattern that refuses a *.server module in a file that is not a server file, which
keeps the move of decision 6 from coming back. No such rule exists yet in @stackra/oxlint-config or .oxlintrc.jsonc.
Linear: STA-340 (0294 Enforcement).