---
title: "Rendering Blocks"
description: "Render Gutenberg blocks as Vue components"
canonical_url: "https://wpnuxt.com/guide/rendering-blocks"
---
# Rendering Blocks

> Render Gutenberg blocks as Vue components

`@wpnuxt/blocks` renders WordPress Gutenberg blocks as individual Vue components.

## When to Use

<table>
<thead>
  <tr>
    <th>
      Approach
    </th>
    
    <th>
      Use Case
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        <WPContent>
      </code>
    </td>
    
    <td>
      <strong>
        Recommended.
      </strong>
      
       Renders blocks or HTML, intercepts internal links for client-side navigation
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        v-sanitize-html
      </code>
    </td>
    
    <td>
      Simple HTML content, no customization needed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @wpnuxt/blocks
      </code>
    </td>
    
    <td>
      Custom rendering per block type, lazy-load images, interactive blocks
    </td>
  </tr>
</tbody>
</table>

## `<WPContent>` Component

`<WPContent>` is the recommended way to render WordPress content. It automatically chooses the best rendering strategy and intercepts internal links for client-side navigation.

### Basic Usage

```vue [pages/[...slug].vue]
<script setup lang="ts">
const route = useRoute()
const { data: page } = await usePageByUri({ uri: route.path })
</script>

<template>
  <WPContent v-if="page" :node="page" />
</template>
```

### How It Works

- When `@wpnuxt/blocks` is installed, renders `editorBlocks` via `BlockRenderer`
- Falls back to `v-sanitize-html` for HTML content
- Clicks on `<a>` tags pointing to your WordPress domain are handled via `navigateTo()` instead of full page reloads
- Modifier keys (Ctrl/Cmd+click), `target="_blank"`, `download`, and `rel="external"` links are not intercepted

### Disabling Link Interception

Link interception is enabled globally by default via the `replaceLinks` config option. To disable it per-instance:

```vue
<WPContent :node="page" :replace-links="false" />
```

See [Configuration](https://wpnuxt.com/reference/configuration) for the global `replaceLinks` option.

### Slot Fallback

Use the default slot to provide custom fallback content:

```vue
<WPContent :node="page">
  <p>No content available.</p>
</WPContent>
```

### Utility Functions

The auto-imported utilities `isInternalLink()` and `toRelativePath()` are available for custom link handling:

```ts
// Check if a URL points to your WordPress site
isInternalLink('/about', wordpressUrl) // true
isInternalLink('https://external.com', wordpressUrl) // false

// Convert absolute WordPress URLs to relative paths
toRelativePath('https://wordpress.example.com/hello-world/') // '/hello-world/'
```

## Install @wpnuxt/blocks

<code-group>

```bash [pnpm]
pnpm add @wpnuxt/blocks
```

```bash [npm]
npm install @wpnuxt/blocks
```

</code-group>

## Configure

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@wpnuxt/core', '@wpnuxt/blocks'],

  wpNuxtBlocks: {
    imageDomains: ['your-wordpress-site.com']
  }
})
```

## WordPress Requirement

Install [WPGraphQL Content Blocks](https://github.com/wpengine/wp-graphql-content-blocks) to expose block data.

<note>

The module automatically checks for the WPGraphQL Content Blocks plugin at startup and warns if it's not installed.

</note>

## GraphQL Fragments

The `@wpnuxt/blocks` module automatically extends the `Post` and `Page` fragments from `@wpnuxt/core` to include `editorBlocks`. No manual configuration is needed.

Run `nuxt prepare` after adding the module to regenerate types.

## Usage

```vue [pages/[...slug].vue]
<script setup lang="ts">
const route = useRoute()
const { data: page } = await usePageByUri({ uri: route.path })
</script>

<template>
  <WPContent v-if="page" :node="page" />
</template>
```

<note>

`<WPContent>` automatically uses `BlockRenderer` when `@wpnuxt/blocks` is installed. You can also use `BlockRenderer` directly if you don't need link interception.

</note>

## Custom Block Components

Override default rendering by creating components in `components/blocks/`:

```vue [components/blocks/CoreParagraph.vue]
<script setup lang="ts">
defineProps<{
  block: {
    attributes?: {
      content?: string
      className?: string
    }
  }
}>()
</script>

<template>
  <p
    :class="['my-custom-paragraph', block.attributes?.className]"
    v-sanitize-html="block.attributes?.content"
  />
</template>
```

## Included Components

- `CoreParagraph`
- `CoreHeading`
- `CoreImage`
- `CoreButton`
- `CoreButtons`
- `CoreGallery`
- `CoreQuote`
- `CoreSpacer`
- `CoreDetails`

Unsupported blocks fall back to `EditorBlock` which renders the saved HTML.

See [Performance](https://wpnuxt.com/guide/performance) for tips on optimizing block rendering, including lazy-loading images and minimizing layout shift.

## Directive

Use `v-sanitize-html` for safe HTML rendering:

```vue
<div v-sanitize-html="block.attributes?.content" />
```

## End-to-End Example

This walkthrough shows how to fetch a page, render its blocks, and customize a specific block type.

### What the GraphQL Data Looks Like

When `@wpnuxt/blocks` is installed, `editorBlocks` is automatically included in page/post queries. The data returned from GraphQL looks like this:

```json
{
  "page": {
    "title": "About Us",
    "editorBlocks": [
      {
        "name": "core/heading",
        "attributes": {
          "level": 2,
          "content": "Our Story"
        }
      },
      {
        "name": "core/paragraph",
        "attributes": {
          "content": "We started building headless WordPress sites in 2023..."
        }
      },
      {
        "name": "core/image",
        "attributes": {
          "url": "https://your-site.com/wp-content/uploads/team.jpg",
          "alt": "Our team",
          "caption": "The team behind WPNuxt"
        }
      },
      {
        "name": "core/buttons",
        "innerBlocks": [
          {
            "name": "core/button",
            "attributes": {
              "text": "Contact Us",
              "url": "/contact"
            }
          }
        ]
      }
    ]
  }
}
```

### Fetching and Rendering

Create a catch-all page that fetches any WordPress page by its URI and renders blocks:

```vue [pages/[...slug].vue]
<script setup lang="ts">
const route = useRoute()
const { data: page } = await usePageByUri({ uri: route.path })

if (!page.value) {
  throw createError({ statusCode: 404, message: 'Page not found' })
}

useHead({ title: page.value.title })
</script>

<template>
  <div>
    <h1>{{ page.value?.title }}</h1>
    <WPContent v-if="page.value" :node="page.value" />
  </div>
</template>
```

`<WPContent>` renders `editorBlocks` via `BlockRenderer` (which maps each block to its Vue component, e.g., `core/heading` → `CoreHeading`) and intercepts internal links for client-side navigation.

### Customizing a Block Type

Say you want to style `core/image` blocks with a caption overlay. Create a custom component:

```vue [components/blocks/CoreImage.vue]
<script setup lang="ts">
defineProps<{
  block: {
    attributes?: {
      url?: string
      alt?: string
      caption?: string
      className?: string
    }
  }
}>()
</script>

<template>
  <figure :class="['relative overflow-hidden rounded-lg', block.attributes?.className]">
    <NuxtImg
      v-if="block.attributes?.url"
      :src="block.attributes.url"
      :alt="block.attributes?.alt || ''"
      class="w-full"
    />
    <figcaption
      v-if="block.attributes?.caption"
      class="absolute bottom-0 left-0 right-0 bg-black/50 text-white p-3 text-sm"
      v-sanitize-html="block.attributes.caption"
    />
  </figure>
</template>
```

Place this file in `components/blocks/` and it automatically overrides the default `CoreImage` component. No registration or configuration needed — Nuxt auto-imports it.

## Related Pages

- **Custom Queries** — Add custom fields to block queries
- **Performance** — Optimize rendering and image loading
- **Images** — Image handling strategies for WordPress content


## Sitemap

See the full [sitemap](https://wpnuxt.com/sitemap.md) for all pages.
