Skip to content

ContentRenderer prose components cause content to disappear during client hydration due to defineAsyncComponent #3743

@iPLAYCAFE

Description

@iPLAYCAFE

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:

  1. Create a Nuxt project with @nuxt/content
  2. Add any markdown file with standard text content (paragraphs, headings, etc.)
  3. Enable SSR (default)
  4. Load the page - text renders correctly via SSR
  5. Observe during client hydration: prose components (ProseP, ProseH2, ProseStrong, etc.) are resolved via defineAsyncComponent(loader) as they are in localComponents, not globalComponents
  6. During the brief async resolution window, the components render empty placeholders, destroying the SSR-rendered DOM
  7. 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:

  1. SSR: Prose components are rendered synchronously - correct HTML output
  2. Client hydration: resolveVueComponent() hits the localComponents branch - wraps each prose component in defineAsyncComponent(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:

  1. htmlTags.has(component) - return as native HTML string (sync)
  2. globalComponents.includes(...) - resolveComponent() (sync)
  3. 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 globalComponents so they resolve synchronously via resolveComponent()
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions