Guide

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:

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

Dynamic Open Graph Images

Use featured images from WordPress as OG images:

<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>
OG images must be absolute URLs. If you're using the proxy approach from the Images guide, make sure to construct the full URL for OG tags.
// 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:

<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 to generate a sitemap from WordPress content:

pnpm add -D @nuxtjs/sitemap
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:

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:

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

useHead({
  link: [
    {
      rel: 'canonical',
      href: `${config.public.siteUrl}${route.path}`,
    },
  ],
})
</script>
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 plugin to set canonical URLs in WordPress.
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 and fetch it using a Custom Query.

Complete Example

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

<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>
  • Custom Queries — Fetch SEO plugin data (Yoast, RankMath) via custom queries
  • Performance — Core Web Vitals and performance optimization
Copyright © 2026