feat(seo): add useSeo composable with JSON-LD and canonical#29
Conversation
Ports upstream docus commit d283f9aa (feat(layer): add more seo optimization) as item #10 from docs/docus-upstream-changes.md. The new useSeo composable in packages/layer/app/composables/useSeo.ts: - Wires reactive title/description through useSeoMeta with OpenGraph + Twitter card meta and an og:url that matches the canonical link - Resolves the canonical URL from useAppConfig().docs.url (populated by modules/config.ts) with an inferSiteURL() fallback - Emits Article or WebSite JSON-LD plus a BreadcrumbList script when the caller supplies a breadcrumb trail - Defers hreflang emission until @nuxtjs/i18n is wired into the layer (TODO comment marks the upstream branch) packages/layer/app/pages/[...slug].vue now uses useSeo with a breadcrumb trail computed from useNavigation + findPageBreadcrumbs (landed in PR #27).
There was a problem hiding this comment.
Code Review
This pull request introduces a new useSeo composable in packages/layer/app/composables/useSeo.ts to handle comprehensive SEO setup for documentation pages, including standard meta tags, canonical links, and JSON-LD structured data. The review feedback highlights a critical security vulnerability regarding potential Cross-Site Scripting (XSS) when injecting unescaped JSON-LD schemas into <script> tags, and suggests a robustness improvement to decouple the breadcrumb schema generation from the page title check.
There was a problem hiding this comment.
2 issues found across 2 files
Architecture diagram
sequenceDiagram
participant Page as [...slug].vue
participant useSeo as useSeo composable
participant useNavigation as useNavigation
participant findBread as findPageBreadcrumbs
participant route as useRoute
participant appConfig as useAppConfig
participant inferURL as inferSiteURL
participant useSeoMeta as useSeoMeta
participant useHead as useHead
participant DOM as DOM
Note over Page,DOM: Page load - docs content route
Page->>useNavigation: get navigation tree
useNavigation-->>Page: navigation data
Page->>findBread: compute breadcrumbs from navigation + route.path
findBread-->>Page: BreadcrumbItem[]
Page->>useSeo: call with title, description, type, publishedAt, modifiedAt, breadcrumbs
useSeo->>route: get current path
route-->>useSeo: route.path
useSeo->>appConfig: get docs.url
appConfig-->>useSeo: configured URL or undefined
alt appConfig.docs.url is empty
useSeo->>inferURL: fallback to env-based inference
inferURL-->>useSeo: resolved base URL
end
useSeo->>useSeo: compute baseUrl (without trailing slash)
useSeo->>useSeo: compute canonicalUrl = joinURL(baseUrl, route.path)
Note over useSeo,useHead: Meta tags (OpenGraph + Twitter)
useSeo->>useSeoMeta: set ogTitle, ogDescription, ogType, ogUrl, ogImage, twitterCard, twitterTitle, twitterDescription, twitterImage
useSeoMeta-->>DOM: meta tags inserted
Note over useSeo,useHead: Canonical link
useSeo->>useHead: set link[rel=canonical] with href=canonicalUrl
useHead-->>DOM: <link rel="canonical"> inserted
Note over useSeo,useHead: JSON-LD structured data
alt type === 'article'
useSeo->>useSeo: build Article schema with headline, description, url, mainEntityOfPage
opt publishedAt provided
useSeo->>useSeo: add datePublished
end
opt modifiedAt provided
useSeo->>useSeo: add dateModified
end
opt siteName available
useSeo->>useSeo: add publisher Organization
end
else type === 'website'
useSeo->>useSeo: build WebSite schema with name, description, url
end
opt breadcrumbs length > 0
useSeo->>useSeo: build BreadcrumbList schema with position, name, item for each breadcrumb
end
useSeo->>useHead: set script[type=application/ld+json] with innerHTML
useHead-->>DOM: <script type="application/ld+json"> inserted
Note over useSeo,DOM: Hreflang deferred (TODO until @nuxtjs/i18n wired)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- Use textContent instead of innerHTML for JSON-LD script tags (XSS-safe per Unhead recommendation; cubic + gemini) - Allow BreadcrumbList schema to render even when title is unset by scoping the title guard to Article/WebSite blocks only (cubic + gemini)
|
* feat(layer): add Vercel markdown-rewrite module
Adds a Nuxt module that injects Vercel build-output rewrite rules so
that AI agents (anything sending `Accept: text/markdown` or
`User-Agent: curl/*`) get served raw markdown instead of the SPA shell.
Behaviour:
- No-op on every non-Vercel preset (matches `vercel`, `vercel-edge`,
`vercel-static`, etc. via `preset.startsWith('vercel')`).
- On Vercel: read `<output.publicDir>/../config.json`, validate that
`llms.txt` was emitted, and unshift route pairs onto `routes` so
they fire before the SPA fallback.
- Rules:
- `/` -> `/llms.txt`
- `/<locale>` -> `/llms.txt` (per `runtimeConfig.public.i18n.locales`)
- `/<page>` -> `/raw<page>.md` for every `/raw/...md` link found
in `llms.txt`
- Vercel's `has` array is AND-ed, so OR semantics between the
`Accept` and `User-Agent` matchers require emitting two rules per
`src` -> `dest` pair.
- Locale codes are regex-escaped before being joined into the
alternation so exotic codes can't break the pattern.
Ports upstream docus commits `6fd8686b` and `9ceafe6f` -- see
`docs/docus-upstream-changes.md` item #9.
Verification:
- `bun lint` -> clean
- `bun typecheck` -> no new errors in `markdown-rewrite.ts`
- `NITRO_PRESET=vercel bun --filter @pleaseai/docs-site build`
injects two routes (homepage / Accept + User-Agent) into
`.vercel/output/config.json`.
- Default (cloudflare) build is unchanged; module bails silently.
* fix(layer): apply review suggestions
Apply review feedback from cubic-dev-ai and gemini-code-assist on
the markdown-rewrite module:
- Read locale config from `nuxt.options.i18n` instead of
`runtimeConfig.public.i18n` (matching the pattern used by the
`nitro:config` hook in nuxt.config.ts). The i18n module does
not always populate `runtimeConfig.public.i18n`, so the previous
source could silently miss locale routes. (cubic)
- Decode the URL pathname with `decodeURIComponent` so that paths
with URL-encoded characters (e.g. `%20`) match Vercel's router,
which compares against decoded request pathnames. (gemini)
- Allow an optional trailing slash on docs page route patterns
(`/?$` instead of `$`) so requests like
`/en/getting-started/installation/` are matched consistently
with the per-locale homepage routes. (cubic, gemini)
* Merge branch 'main' into worktree-agent-a1a3a89e87faf9e6d
Resolve modules-array conflict in packages/layer/nuxt.config.ts by
keeping both new module registrations from #29/#30/#31 and this PR's
markdown-rewrite module.



Summary
Ports upstream docus commit
d283f9aa(feat(layer): add more seo optimization) — item #10 fromdocs/docus-upstream-changes.md.Follows on from #27 (first docus upstream bundle).
What's in the box
packages/layer/app/composables/useSeo.ts(auto-imported)title/descriptionflowing intouseSeoMetawith full OpenGraph + Twitter card meta<link>resolved fromuseAppConfig().docs.url(populated bymodules/config.ts) with aninferSiteURL()fallback so SSR works in CI environments whereapp.configisn't pre-populatedArticleorWebSiteschema (chosen bytype) plus an optionalBreadcrumbListscript when the caller supplies a trailpackages/layer/app/pages/[...slug].vuenow callsuseSeowith breadcrumbs computed fromuseNavigation+findPageBreadcrumbs(both already landed in feat(layer): port first docus upstream bundle #27)Hreflang deferral
Upstream's
useSeoalso emits<link rel="alternate" hreflang="...">tags driven byuseDocusI18n/runtimeConfig.public.i18n. Our layer doesn't ship@nuxtjs/i18nyet, so the hreflang branch is left as aTODOcomment insideuseSeo.ts— to be wired up alongside the i18n module work tracked separately in the survey.Verification
Type of change
Summary by cubic
Adds a
useSeocomposable to set canonical links, OpenGraph/Twitter meta, and JSON-LD (Article/WebSite + Breadcrumbs) for better SEO. Docs pages now useuseSeowith computed breadcrumbs.New Features
useAppConfig().docs.urlwithinferSiteURL()fallback.ArticleorWebSite, plusBreadcrumbListwhen breadcrumbs are provided.title/description, with optionalogImage,publishedAt, andmodifiedAt.[...slug].vuecomputes breadcrumbs viauseNavigation+findPageBreadcrumbsand callsuseSeo.@nuxtjs/i18nis added.Bug Fixes
textContent(notinnerHTML) for JSON-LD scripts to avoid XSS.BreadcrumbListto render even whentitleis unset.Written for commit a50c22e. Summary will update on new commits.