SEO & Meta Tags
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>
.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>
// 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
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:
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>
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>
Related Pages
- Custom Queries — Fetch SEO plugin data (Yoast, RankMath) via custom queries
- Performance — Core Web Vitals and performance optimization