---
title: "SEO & Meta Tags"
description: "Optimize your headless WordPress site for search engines"
canonical_url: "https://wpnuxt.com/guide/seo"
---
# SEO & Meta Tags

> Optimize your headless WordPress site for search engines

WPNuxt sites are server-rendered by default, making them SEO-friendly out of the box. This guide covers how to use WordPress content data for meta tags, Open Graph images, structured data, and sitemaps.

## Page Meta from WordPress Data

Use Nuxt's `useHead()` and `useSeoMeta()` composables with WordPress post data:

```vue
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useNodeByUri({ uri: route.path })

useSeoMeta({
  title: post.value?.title,
  description: post.value?.excerpt?.replace(/<[^>]+>/g, '').trim(),
  ogTitle: post.value?.title,
  ogDescription: post.value?.excerpt?.replace(/<[^>]+>/g, '').trim(),
  ogImage: post.value?.featuredImage?.node?.sourceUrl,
  ogType: 'article',
  articlePublishedTime: post.value?.date,
  articleModifiedTime: post.value?.modified,
})

useHead({
  title: post.value?.title,
})
</script>
```

<callout type="info">

WordPress excerpts contain HTML tags. Strip them with `.replace(/<[^>]+>/g, '').trim()` when using them as meta descriptions.

</callout>

## Dynamic Open Graph Images

Use featured images from WordPress as OG images:

```vue
<script setup lang="ts">
const { data: post } = await usePostByUri({ uri: route.path })
const config = useRuntimeConfig()

const featuredImage = computed(() => post.value?.featuredImage?.node)

useSeoMeta({
  ogImage: featuredImage.value?.sourceUrl,
  ogImageWidth: featuredImage.value?.mediaDetails?.width,
  ogImageHeight: featuredImage.value?.mediaDetails?.height,
  ogImageAlt: featuredImage.value?.altText,
  twitterCard: 'summary_large_image',
  twitterImage: featuredImage.value?.sourceUrl,
})
</script>
```

<callout type="warning">

OG images must be absolute URLs. If you're using the proxy approach from the [Images guide](https://wpnuxt.com/guide/images), make sure to construct the full URL for OG tags.

</callout>

```ts
// Construct absolute URL for OG image when using proxied images
const ogImageUrl = featuredImage.value?.sourceUrl
  ? `${config.public.siteUrl}${getRelativeImagePath(featuredImage.value.sourceUrl)}`
  : undefined
```

## Structured Data (JSON-LD)

Add structured data for blog posts to improve search result appearance:

```vue
<script setup lang="ts">
const { data: post } = await usePostByUri({ uri: route.path })
const config = useRuntimeConfig()

useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: post.value?.title,
        description: post.value?.excerpt?.replace(/<[^>]+>/g, '').trim(),
        image: post.value?.featuredImage?.node?.sourceUrl,
        datePublished: post.value?.date,
        dateModified: post.value?.modified,
        author: {
          '@type': 'Person',
          name: post.value?.author?.node?.name,
        },
      }),
    },
  ],
})
</script>
```

For your site's homepage or blog listing, use `WebSite` or `CollectionPage` instead of `BlogPosting`.

## Sitemap Generation

Use [`@nuxtjs/sitemap`](https://nuxtseo.com/sitemap) to generate a sitemap from WordPress content:

```bash
pnpm add -D @nuxtjs/sitemap
```

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

  site: {
    url: 'https://your-nuxt-site.com',
  },
})
```

Nuxt Sitemap automatically discovers pre-rendered routes. For dynamic routes from WordPress, add a server route:

```ts [server/api/__sitemap__/urls.ts]
import { defineSitemapEventHandler } from '#imports'

export default defineSitemapEventHandler(async () => {
  const response = await fetch('https://your-wordpress.com/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `{
        posts(first: 100) { nodes { uri modified } }
        pages(first: 100) { nodes { uri modified } }
      }`,
    }),
  })

  const { data } = await response.json()
  const posts = data?.posts?.nodes || []
  const pages = data?.pages?.nodes || []

  return [...posts, ...pages].map((node) => ({
    loc: node.uri,
    lastmod: node.modified,
  }))
})
```

## Canonical URLs

In a headless setup, both WordPress and your Nuxt site could serve the same content. Set canonical URLs to prevent duplicate content issues:

```vue
<script setup lang="ts">
const route = useRoute()
const config = useRuntimeConfig()

useHead({
  link: [
    {
      rel: 'canonical',
      href: `${config.public.siteUrl}${route.path}`,
    },
  ],
})
</script>
```

<callout type="warning">

Make sure WordPress doesn't expose your content publicly, or configure it to point canonical URLs to your Nuxt frontend. You can use the [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) plugin to set canonical URLs in WordPress.

</callout>

<callout type="tip">

If you use a WordPress SEO plugin like Yoast or RankMath, you can expose its data via GraphQL with plugins like [WPGraphQL for Yoast SEO](https://github.com/developer-developer/developer-developer) and fetch it using a [Custom Query](https://wpnuxt.com/guide/custom-queries).

</callout>

## Complete Example

Here's an SEO-optimized blog post page combining all techniques:

```vue
<script setup lang="ts">
const route = useRoute()
const config = useRuntimeConfig()
const siteUrl = config.public.siteUrl || ''

const { data: post } = await useNodeByUri({ uri: route.path })

const title = computed(() => post.value?.title || '')
const description = computed(() =>
  post.value?.excerpt?.replace(/<[^>]+>/g, '').trim() || ''
)
const imageUrl = computed(() =>
  post.value?.featuredImage?.node?.sourceUrl || ''
)

// Meta tags
useSeoMeta({
  title: title.value,
  description: description.value,
  ogTitle: title.value,
  ogDescription: description.value,
  ogImage: imageUrl.value,
  ogUrl: `${siteUrl}${route.path}`,
  ogType: 'article',
  articlePublishedTime: post.value?.date,
  twitterCard: 'summary_large_image',
})

// Canonical URL
useHead({
  title: title.value,
  link: [{ rel: 'canonical', href: `${siteUrl}${route.path}` }],
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: title.value,
        description: description.value,
        image: imageUrl.value,
        datePublished: post.value?.date,
        dateModified: post.value?.modified,
        author: {
          '@type': 'Person',
          name: post.value?.author?.node?.name,
        },
        publisher: {
          '@type': 'Organization',
          name: config.public.siteName || 'My Site',
        },
      }),
    },
  ],
})
</script>
```

## Related Pages

- **Custom Queries** — Fetch SEO plugin data (Yoast, RankMath) via custom queries
- **Performance** — Core Web Vitals and performance optimization


## Sitemap

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