Skip to content

fix(ssr): local content components on Cloudflare Workers#3704

Open
onmax wants to merge 22 commits intonuxt:mainfrom
onmax:fix/ssr-local-components-cloudflare
Open

fix(ssr): local content components on Cloudflare Workers#3704
onmax wants to merge 22 commits intonuxt:mainfrom
onmax:fix/ssr-local-components-cloudflare

Conversation

@onmax
Copy link
Contributor

@onmax onmax commented Jan 26, 2026

Summary

  • Restore lazy loading for local content components on Cloudflare Workers by introducing localComponentLoaders.
  • Avoid runtime import('#content/components') in ContentRenderer.
  • Fail fast with a clear error when a named export is missing.

Why

Cloudflare Workers SSR fails to render local MDC components when #content/components is dynamically imported. Loading local components via per-component async loaders avoids the Worker import issue while keeping SSR intact.

Previews (repro on same worker)

  • Main (released @nuxt/content@3.11.0) fails SSR for local components: preview
    • SSR HTML does not include SSR: Component rendered on server.
  • This PR fixes SSR: preview
    • SSR HTML does include SSR: Component rendered on server.

Tests

  • Manual CF Workers preview SSR check (main vs PR, same worker + D1).
  • Manual dev check with @nuxtjs/seo enabled (no missing-export errors).

@vercel
Copy link

vercel bot commented Jan 26, 2026

@onmax is attempting to deploy a commit to the Nuxt Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 26, 2026

npm i https://pkg.pr.new/@nuxt/content@3704

commit: 9f321e2

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Runtime component resolution now reads a generated localComponentLoaders map from #content/components and conditionally wraps found loaders with defineAsyncComponent. Template generation (src/utils/templates.ts) was changed to emit concrete ESM imports, include explicit export names, export localComponentLoaders, and add a pickExport helper that throws when an expected export is missing. Types (src/types/global.d.ts) declare localComponentLoaders. Nuxt Studio integration and default collection handling were added (src/utils/config.ts, new src/utils/studio.ts). Tests, docs, changelog, and package version were updated; the knitwork dependency was removed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(ssr): local content components on Cloudflare Workers' directly and specifically describes the main change: restoring SSR for local content components on Cloudflare Workers.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the SSR fix for Cloudflare Workers, the approach taken (localComponentLoaders), and providing clear motivation and test evidence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mateusznarowski
Copy link

Now, the application built with the cloudflare_module preset renders correctly on the server side (so that part can be considered fixed), but an error appears in dev on the client side, the page flickers, and a 500 error appears with the following message:

The requested module '/_nuxt/@fs/Users/mateusznarowski/temp/content-issue/node_modules/.pnpm/nuxt@4.3.0_@parcel+watcher@2.5.6_@vue+compiler-sfc@3.5.27_better-sqlite3@12.6.2_cac@6.7_d2aeb2292c1af4998397e345ba461360/node_modules/nuxt/dist/app/components/nuxt-stubs.js?v=9bbd892e' does not provide an export named 'default'

Nuxt DevTools

fs/Users/mateusznarowski/temp/content-issue/node_modules/.pnpm/nuxt@4.3.0_@parcel+watcher@2.5.6_@vue+compiler-sfc@3.5.27_better-sqlite3@12.6.2_cac@6.7_d2aeb2292c1af4998397e345ba461360/node_modules/nuxt/dist/app/components/nuxt-stubs.js?v=9bbd892e' does not provide an export named 'default':undefined:undefined

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/templates.ts`:
- Around line 150-151: The generated re-export line in the localComponents
mapping uses a raw single-quoted path which breaks if the path contains a single
quote; change the template generation in templates.ts where localComponents.map
is used (the arrow mapping that produces `export { ${exp} as ${pascalName} }
from '${path}'`) to serialize/escape the path with JSON.stringify(path) so the
emitted import string is properly quoted and escaped; update that mapping
expression to use the JSON.stringify-wrapped path reference.
🧹 Nitpick comments (1)
src/utils/templates.ts (1)

138-144: Introduce a typed tuple for component entries.
Using unknown[] with positional indices makes the new export field fragile and hard to read. A typed tuple clarifies intent and avoids unsafe indexing.

♻️ Proposed refactor
+type ComponentEntry = [pascalName: string, path: string, global: boolean, exportName: string]
+
-        }, {} as Record<string, unknown[]>)
+        }, {} as Record<string, ComponentEntry>)

@mateusznarowski
Copy link

The clean reproduction I provided in the issue is now OK. I wanted to test the fix in my project where I use the @nuxtjs/seo module, and I get the following error

The requested module ‘/_nuxt/@fs/Users/mateusznarowski/temp/content-issue/node_modules/.cache/vite/client/deps/@unhead_schema-org_vue.js?v=67c7599a’ does not provide an export named 'SchemaOrgDebug'

To trigger the error, on a clean reproduction without any additional configuration simply add the module in nuxt.config and run dev.

@mateusznarowski
Copy link

Before applying the patch, the component imports in the ./.nuxt/content/components.ts folder look like this:

export const CustomComponent = () => import(‘./../../app/components/content/CustomComponent.vue’)

and after applying the patch, they look like this:

export { default as CustomComponent } from ‘./../../app/components/content/CustomComponent.vue’

I haven't analyzed this in depth, but won't this have an impact on performance? I also wondered what works differently in the cloudflare_module preset compared to others that such a change is required?

@onmax
Copy link
Contributor Author

onmax commented Jan 29, 2026

Dynamic imports inside render functions don't work reliably in CF Workers runtime. Direct imports work, but lose lazy loading benefit.
There are other works around

  • Keep lazy loading for dev, direct for CF Workers. Use preset detection to choose strategy:
const useLazyLoading = nuxt.options.nitro.preset !== 'cloudflare_module'
  • Namespace imports in ContentRenderer. Keep lazy loading but use namespace import workaround for CF Workers:
 const comp = await import('./specific-component.vue')

I would go with the first one, but it is risky since is different behavior per env.

@farnabaz
Copy link
Member

farnabaz commented Feb 5, 2026

Changing dynamic import into static import is not ideal solution, it will load all components when ContentRenderer is rendering. This is not ideal for large amount of components like Nuxt UI docs website.

We should find a way to sole dynamic import problem.

@onmax
Copy link
Contributor Author

onmax commented Feb 5, 2026

Ready for review. I added two Cloudflare preview links in the PR body: main (fails SSR) and this branch (SSR fixed).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/content/docs/7.integrations/01.i18n.md`:
- Around line 89-92: The computed slug currently assumes route.params.slug
exists and will produce "/undefined" for root routes; update the slug computed
(the computed() that references route.params.slug and withLeadingSlash) to
handle undefined by using a safe fallback (e.g., treat undefined as an empty
string) before joining or stringifying; check Array.isArray(route.params.slug)
and if undefined return withLeadingSlash('') or similar so useRoute/useI18n
logic yields "/" instead of "/undefined".
🧹 Nitpick comments (3)
CHANGELOG.md (1)

3-11: Consider noting the Cloudflare Workers SSR/local components fix.

This PR’s core change (local content components SSR on Cloudflare Workers) isn’t reflected in the 3.11.1 bug-fix list. Adding a bullet will make the release notes more complete.

📝 Suggested changelog addition
 ### Bug Fixes
 
 * issue with disabling `contentRawMarkdown` ([5be6b0c](https://github.com/nuxt/content/commit/5be6b0cdf5d50440010a988dfd472e69d989f3ef))
+* fix SSR of local content components on Cloudflare Workers (local component loaders)
src/utils/config.ts (1)

74-74: Redundant fallback to useNuxt().

The nuxt parameter is a required argument of loadContentConfig, so it should always be defined. The || useNuxt() fallback is unnecessary and could cause confusion.

Suggested fix
-  if (hasNuxtModule('nuxt-studio', nuxt || useNuxt())) {
+  if (hasNuxtModule('nuxt-studio', nuxt)) {
src/utils/studio.ts (1)

52-67: Document the in-place mutation.

The function mutates collectionsConfig directly (adding exclude patterns). While this is acceptable given the call site in config.ts, consider adding a brief JSDoc note to make the side effect explicit for future maintainers.

Suggested documentation enhancement
 /**
  * Resolves studio collection configuration when nuxt-studio is installed.
  * Automatically creates a studio collection and adds exclude patterns to other collections.
+ *
+ * `@remarks` Mutates `collectionsConfig` in place.
  */
 export function resolveStudioCollection(

Comment on lines 89 to 92
const route = useRoute()
const { locale } = useI18n()
const slug = computed(() => withLeadingSlash(String(route.params.slug)))
const slug = computed(() => Array.isArray(route.params.slug) ? withLeadingSlash(String(route.params.slug.join('/'))) : withLeadingSlash(String(route.params.slug)))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle undefined slug to avoid /undefined on root routes.

In catch-all routes, route.params.slug can be undefined (e.g., /). The current snippet would produce /undefined. Consider a safe fallback.

💡 Suggested adjustment
-const slug = computed(() => Array.isArray(route.params.slug) ? withLeadingSlash(String(route.params.slug.join('/'))) : withLeadingSlash(String(route.params.slug)))
+const slug = computed(() => {
+  const raw = route.params.slug
+  const path = Array.isArray(raw) ? raw.join('/') : (raw ?? '')
+  return withLeadingSlash(String(path))
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const route = useRoute()
const { locale } = useI18n()
const slug = computed(() => withLeadingSlash(String(route.params.slug)))
const slug = computed(() => Array.isArray(route.params.slug) ? withLeadingSlash(String(route.params.slug.join('/'))) : withLeadingSlash(String(route.params.slug)))
const route = useRoute()
const { locale } = useI18n()
const slug = computed(() => {
const raw = route.params.slug
const path = Array.isArray(raw) ? raw.join('/') : (raw ?? '')
return withLeadingSlash(String(path))
})
🤖 Prompt for AI Agents
In `@docs/content/docs/7.integrations/01.i18n.md` around lines 89 - 92, The
computed slug currently assumes route.params.slug exists and will produce
"/undefined" for root routes; update the slug computed (the computed() that
references route.params.slug and withLeadingSlash) to handle undefined by using
a safe fallback (e.g., treat undefined as an empty string) before joining or
stringifying; check Array.isArray(route.params.slug) and if undefined return
withLeadingSlash('') or similar so useRoute/useI18n logic yields "/" instead of
"/undefined".

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.

9 participants