feat(layer): port first docus upstream bundle#27
Conversation
Port five framework-agnostic changes from docus upstream that don't depend on @nuxt/ui or @nuxtjs/i18n. See docs/docus-upstream-changes.md for the full survey. - inferSiteURL: detect VERCEL_PROJECT_PRODUCTION_URL / VERCEL_BRANCH_URL / VERCEL_URL / NUXT_PUBLIC_SITE_URL and normalize with ufo.withHttps - pages/[...slug]: register /raw/*.md prerender path so static builds expose markdown variants for AI agents (docus 9ceafe6f) - content.config: skip landing collection when app/pages/index.vue exists; auto-prefix /docs when content/docs/ exists (docus f9e999d8, 99c78508). New helpers in utils/pages.ts (landingPageExists, docsFolderExists). - app/utils/navigation: add transformNavigation + findPageBreadcrumbs with BreadcrumbItem type - server/routes/sitemap.xml: serve a sitemap derived from content collections, skipping .navigation paths and sitemap:false pages (docus 45bffbc5, cd2c62e4)
There was a problem hiding this comment.
Code Review
This pull request introduces several enhancements to the layer, including new navigation utilities, dynamic collection definitions in content.config.ts based on the existence of a landing page or docs folder, a dynamic XML sitemap generator route, and improved site URL inference. The review highlights three critical issues: first, joinURL from ufo is used for filesystem path operations, which can fail on Windows and should be replaced with standard path-joining utilities; second, the sitemap generator queries sitemap and modifiedAt fields which are missing from the docsSchema definition; and third, the sitemap generator attempts to query localized collections that are not registered in the content configuration.
| const availableLocales = getAvailableLocales(config.public as Record<string, unknown>) | ||
| const collections: string[] = availableLocales.length > 0 | ||
| ? availableLocales.map(locale => `docs_${locale}`) | ||
| : ['docs'] | ||
|
|
||
| if (availableLocales.length > 0) { | ||
| for (const locale of availableLocales) { | ||
| collections.push(`landing_${locale}`) | ||
| } | ||
| } | ||
| else { | ||
| collections.push('landing') | ||
| } |
There was a problem hiding this comment.
When availableLocales is not empty, this route attempts to query localized collections like docs_en or landing_en. However, packages/layer/content.config.ts only defines the base docs and landing collections and does not dynamically register localized collections based on the configured locales.
As a result, queryCollection will throw an error for these localized collections, which is caught and silently ignored, leading to an empty sitemap on localized sites.
To resolve this, you should dynamically define the localized collections in packages/layer/content.config.ts by reading the configured locales from the Nuxt options, or ensure they are registered.
There was a problem hiding this comment.
[claude] ⏭️ Not applied in this PR
- Reason: Dynamic i18n collection registration in
content.config.tsis explicitly scoped to a follow-up per the survey doc (docs/docus-upstream-changes.md) —@nuxtjs/i18nisn't installed yet and the layer has no i18n active today, soavailableLocalesis always empty here and the localized branch never executes. - Alternative: The
try/catcharoundqueryCollectionalready swallows missing-collection errors. I updated the comment to mention "or i18n locale collections are not registered yet" so the intent is documented.
Thanks @gemini-code-assist — this will become valid once we land the i18n port.
There was a problem hiding this comment.
Thank you for the clarification, @amondnet. That makes perfect sense. Given that the i18n infrastructure is currently out of scope for this PR and the try/catch block safely handles the missing collections, the current implementation is acceptable. I appreciate you documenting the intent in the comments.
|
@cubic-dev-ai review |
@amondnet I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
1 issue found across 6 files
Architecture diagram
sequenceDiagram
participant Dev as Developer
participant App as Nuxt App
participant Layer as Docus Layer
participant FS as File System
participant Content as Nuxt Content
participant Nav as Navigation Utils
participant DB as In-Memory Store
participant SSG as Static Generator
participant Client as Browser/Agent
Note over Dev,Client: Build-time Configuration Phase
Dev->>App: Start dev/build
App->>Layer: Initialize layer (content.config.ts)
Layer->>FS: Check landing page exists
FS-->>Layer: landingPageExists()
alt Landing page exists
Layer->>Layer: Skip landing collection
else No landing page
Layer->>Content: Define landing collection
end
Layer->>FS: Check docs folder exists
FS-->>Layer: docsFolderExists()
alt Docs folder exists
Layer->>Content: Define docs collection with /docs prefix
else Flat content
Layer->>Content: Define docs collection with / prefix
end
Note over Dev,Client: Runtime Request Phase
Client->>App: Request page
App->>Layer: Load page component
Layer->>Content: Query content collections
Content-->>Layer: Page data
alt Server-side prerender
Layer->>SSG: prerenderRoutes("/raw/*.md")
SSG-->>Client: Static .md file
end
Client->>App: Request /sitemap.xml
App->>Layer: sitemap.xml.ts handler
Layer->>Layer: inferSiteURL() with env vars
alt NUXT_PUBLIC_SITE_URL set
Layer->>Layer: Use as-is
else VERCEL_URL set
Layer->>Layer: withHttps() applied
end
Layer->>Content: Query all collections
Content-->>Layer: Pages with path, modifiedAt
Layer->>Layer: Filter navigation paths & excluded
Layer->>Layer: Build XML with escapeXml()
Layer-->>Client: XML sitemap response
Client->>App: Navigate to /docs/guide
App->>Nav: transformNavigation()
Nav->>Nav: Strip locale/docs level
alt i18n enabled
Nav->>Nav: Find locale-specific navigation
else No i18n
Nav->>Nav: Find /docs children
end
Nav-->>App: Clean navigation tree
App->>Nav: findPageBreadcrumbs(navigation, path)
Nav->>Nav: Walk tree recursively
Nav-->>App: Breadcrumb trail
App-->>Client: Rendered page with navigation & breadcrumbs
opt Sitemap generation failure
Layer->>Layer: Catch collection not found (skip silently)
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- utils/pages.ts: use node:path.join instead of ufo.joinURL for filesystem paths (cross-platform; @gemini-code-assist) - content.config.ts: declare optional 'sitemap' field on docs schema so frontmatter sitemap:false actually opts pages out (@gemini-code-assist) - server/routes/sitemap.xml.ts: drop modifiedAt handling (not in schema, would be undefined); return 404 when site URL is missing so we never emit relative <loc> entries (@cubic-dev-ai)
Two issues surfaced in the Cloudflare Pages deploy of #27: 1. Crawler-discovered section index routes (e.g. /docs/getting-started) render via [...slug].vue even though no source markdown exists. The unconditional addPrerender call I added in PR #27 then registered /raw/docs/getting-started.md, which the raw handler 404s, failing the build with 'fatal: Page not found'. Gate the registration on page.value so we only enqueue raw paths for pages that actually have a source file. 2. The new sitemap.xml route imported inferSiteURL from utils/meta, which also exports getPackageJsonMetadata (node:fs/promises + node:path). That pulled node:* modules into the Worker bundle and broke the Cloudflare publish step ('No such module node:buffer'). Split inferSiteURL out into a runtime-safe utils/site-url module and re-export it from utils/meta so existing build-time callers keep working.
|
* feat(seo): add useSeo composable with JSON-LD and canonical 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). * fix(seo): apply review suggestions - 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)



Summary
First bundle of ports from
ref/docusupstream (v5.4.1 → v5.11.0+main). Picks five framework-agnostic changes that don't depend on@nuxt/uior@nuxtjs/i18n, so they apply cleanly to our shadcn-vue + Tailwind v4 layer.Survey:
docs/docus-upstream-changes.mdChanges
inferSiteURLenv var coverage +withHttpspackages/layer/utils/meta.ts/raw/*.mdprerender pathpackages/layer/app/pages/[...slug].vue9ceafe6fcontent.configauto-detect +utils/pages.tspackages/layer/content.config.ts,packages/layer/utils/pages.tsf9e999d8,99c78508transformNavigation+findPageBreadcrumbspackages/layer/app/utils/navigation.tssitemap.xmlroutepackages/layer/server/routes/sitemap.xml.ts45bffbc5,cd2c62e4Behavioural notes
landingcollection becomes opt-out: if the consuming app shipsapp/pages/index.vue, the layer skips defining alandingcollection. Existing apps with only acontent/index.mdare unaffected./docsauto-prefix: ifcontent/docs/exists, the docs collection sources from there and uses/docsas the prefix. Flatcontent/layouts continue to work as before./sitemap.xmlto render; nothing else references it yet.Verification
bun lint— clean (only an unrelated baseline-browser-mapping data warning)cd packages/layer && bun typecheck— no new errors (pre-existing errors inAppHeaderLogo,AppSearch,ComponentsList,DialogScrollContent,modules/config.ts,nuxt.config.ts,pages/[...slug].vueTOC code remain; none touched here)Not included (deferred to follow-ups)
Per the survey, these need additional dependencies or stack work:
useSeoJSON-LD composable (hreflang requires@nuxtjs/i18n)dshortcut for color mode (needs vueuse-based reimplementation)