-
-
Notifications
You must be signed in to change notification settings - Fork 743
Description
Environment
- Operating System: Windows 11 x64
- Node Version: v25.7.0
- Nuxt Version: 4.3.1
- @nuxt/content: 3.12.0
- Package Manager: bun@1.3.10
- Builder: vite
Version
3.12.0
Reproduction
This is a source-level issue in ContentRenderer.vue (no external reproduction needed as the root cause is clearly identifiable in the source code).
The problematic code is in src/runtime/components/ContentRenderer.vue, around line 126:
if (globalComponents.includes(pascalCase(component))) {
_component = resolveComponent(component, false);
} else if (localComponents.includes(pascalCase(component))) {
const loader = localComponentLoaders[pascalCase(component)];
_component = loader ? defineAsyncComponent(loader) : void 0;
}Steps to reproduce:
- Create a Nuxt project with
@nuxt/content - Add any markdown file with standard text content (paragraphs, headings, etc.)
- Enable SSR (default)
- Load the page - text renders correctly via SSR
- Observe during client hydration: prose components (ProseP, ProseH2, ProseStrong, etc.) are resolved via
defineAsyncComponent(loader)as they are inlocalComponents, notglobalComponents - During the brief async resolution window, the components render empty placeholders, destroying the SSR-rendered DOM
- Text content disappears momentarily or permanently depending on timing
Description
Problem:
When ContentRenderer hydrates on the client, prose components (ProseP, ProseH2, ProseStrong, etc.) are resolved as local components via defineAsyncComponent(). This creates a timing issue:
- SSR: Prose components are rendered synchronously - correct HTML output
- Client hydration:
resolveVueComponent()hits thelocalComponentsbranch - wraps each prose component indefineAsyncComponent(loader)- during the async resolution window, components render empty placeholders - SSR DOM is destroyed
This causes all text content rendered through ContentRenderer to briefly disappear (or permanently disappear if the component resolution race condition hits at the wrong time).
Root Cause:
In ContentRenderer.vue, the resolveVueComponent function handles component resolution in this order:
htmlTags.has(component)- return as native HTML string (sync)globalComponents.includes(...)-resolveComponent()(sync)localComponents.includes(...)-defineAsyncComponent(loader)(async - causes hydration mismatch)
Prose components (ProseP, ProseH2, etc.) fall into category 3. During SSR they render fine, but during client hydration the async wrapper creates empty placeholders that don't match the SSR output.
Workaround:
Setting :prose="false" on ContentRenderer forces all elements to render as native HTML tags (category 1), bypassing the async component resolution entirely:
<ContentRenderer :value="content" :prose="false" />This works but loses the ability to customize prose component rendering.
Suggested Fix:
Consider either:
- Moving prose components to
globalComponentsso they resolve synchronously viaresolveComponent() - Using synchronous resolution for prose components during hydration (e.g., check `import.meta.client" and use eager loading)
- Pre-loading prose component chunks during SSR so they are available immediately during hydration
Additional context
This issue is particularly impactful when ContentRenderer is used with Nuxt Studio, where the client-side SQLite database re-initialization triggers a full re-render of ContentRenderer, making the async prose resolution race condition more likely to cause permanent content disappearance.
The :prose="false" workaround is currently the only reliable way to prevent content from disappearing during hydration when using `ContentRenderer" with SSR.