Skip to content

feat(seo): add useSeo composable with JSON-LD and canonical#29

Merged
amondnet merged 2 commits into
mainfrom
feat/use-seo-composable
May 28, 2026
Merged

feat(seo): add useSeo composable with JSON-LD and canonical#29
amondnet merged 2 commits into
mainfrom
feat/use-seo-composable

Conversation

@amondnet
Copy link
Copy Markdown
Contributor

@amondnet amondnet commented May 28, 2026

Summary

Ports upstream docus commit d283f9aa (feat(layer): add more seo optimization) — item #10 from docs/docus-upstream-changes.md.

Follows on from #27 (first docus upstream bundle).

What's in the box

  • New packages/layer/app/composables/useSeo.ts (auto-imported)
    • Reactive title / description flowing into useSeoMeta with full OpenGraph + Twitter card meta
    • Canonical <link> resolved from useAppConfig().docs.url (populated by modules/config.ts) with an inferSiteURL() fallback so SSR works in CI environments where app.config isn't pre-populated
    • JSON-LD Article or WebSite schema (chosen by type) plus an optional BreadcrumbList script when the caller supplies a trail
  • packages/layer/app/pages/[...slug].vue now calls useSeo with breadcrumbs computed from useNavigation + findPageBreadcrumbs (both already landed in feat(layer): port first docus upstream bundle #27)

Hreflang deferral

Upstream's useSeo also emits <link rel="alternate" hreflang="..."> tags driven by useDocusI18n / runtimeConfig.public.i18n. Our layer doesn't ship @nuxtjs/i18n yet, so the hreflang branch is left as a TODO comment inside useSeo.ts — to be wired up alongside the i18n module work tracked separately in the survey.

Verification

bun lint        # passes
bun typecheck   # passes for new code (pre-existing errors unchanged)
bun dev         # http://localhost:3001/docs/getting-started/introduction shows:
                #   <link rel="canonical" ...>
                #   <meta property="og:*" ...>
                #   <meta name="twitter:*" ...>
                #   <script type="application/ld+json"> Article + BreadcrumbList

Type of change

  • New feature (non-breaking change which adds functionality)

Summary by cubic

Adds a useSeo composable to set canonical links, OpenGraph/Twitter meta, and JSON-LD (Article/WebSite + Breadcrumbs) for better SEO. Docs pages now use useSeo with computed breadcrumbs.

  • New Features

    • Canonical URL resolved from useAppConfig().docs.url with inferSiteURL() fallback.
    • JSON-LD scripts: Article or WebSite, plus BreadcrumbList when breadcrumbs are provided.
    • OpenGraph and Twitter meta from reactive title/description, with optional ogImage, publishedAt, and modifiedAt.
    • [...slug].vue computes breadcrumbs via useNavigation + findPageBreadcrumbs and calls useSeo.
    • Hreflang tags deferred until @nuxtjs/i18n is added.
  • Bug Fixes

    • Use textContent (not innerHTML) for JSON-LD scripts to avoid XSS.
    • Allow BreadcrumbList to render even when title is unset.

Written for commit a50c22e. Summary will update on new commits.

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).
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/layer/app/composables/useSeo.ts
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 28, 2026

Deploying docs-please with  Cloudflare Pages  Cloudflare Pages

Latest commit: a50c22e
Status:⚡️  Build in progress...

View logs

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layer/app/composables/useSeo.ts Outdated
Comment thread packages/layer/app/composables/useSeo.ts Outdated
@amondnet amondnet self-assigned this May 28, 2026
- 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)
@sonarqubecloud
Copy link
Copy Markdown

@amondnet amondnet merged commit 1267889 into main May 28, 2026
2 of 3 checks passed
@amondnet amondnet deleted the feat/use-seo-composable branch May 28, 2026 15:56
amondnet added a commit that referenced this pull request May 28, 2026
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.
amondnet added a commit that referenced this pull request May 28, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant