SEO is crucial for web applications, but implementing it correctly can be challenging. In this article, we'll explore how to create a comprehensive SEO solution using Nuxt 3, including meta tags, Open Graph images, JSON-LD structured data, and dynamic OG image generation.
π― What We'll Build
We'll create a complete SEO implementation featuring:
- Meta tags for search engines
- Open Graph and Twitter Card tags for social sharing
- JSON-LD structured data for rich snippets
- Dynamic OG image generation using Satori
- Canonical URLs for duplicate content prevention
ποΈ Project Setup
First, let's set up our Nuxt 3 project with the necessary dependencies:
{
"dependencies": {
"@prisma/client": "^6.16.2",
"@resvg/resvg-js": "^2.6.2",
"nuxt": "^4.1.2",
"satori": "^0.18.3",
"satori-html": "^0.3.2",
"vue": "^3.5.21"
},
"devDependencies": {
"@nuxt/image": "^1.11.0"
}
}
βοΈ Nuxt Configuration
Configure your runtime settings in nuxt.config.ts
:
export default defineNuxtConfig({
devtools: { enabled: true },
pages: true,
modules: ['@nuxt/image'],
runtimeConfig: {
public: {
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'
}
},
experimental: {
payloadExtraction: false
},
nitro: {
preset: 'node-server',
experimental: { wasm: true }
}
})
The siteUrl
configuration is essential for generating absolute URLs for canonical links and Open Graph images.
π SEO Meta Implementation
Here's our complete SEO demo page (app/pages/seo-demo.vue
):
<script setup lang="ts">
const route = useRoute()
const siteUrl = useRuntimeConfig().public.siteUrl
const canonical = new URL(route.fullPath || '/seo-demo', siteUrl).toString()
const title = "Nuxt SEO Demo"
const description = "Example page showing proper meta tags and JSON-LD."
const ogImage = `${siteUrl}/api/og?title=${encodeURIComponent(title)}`
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
ogUrl: canonical,
ogType: 'website',
ogImage,
twitterCard: 'summary_large_image',
twitterTitle: title,
twitterDescription: description,
twitterImage: ogImage
})
useHead({
link: [{ rel: 'canonical', href: canonical }],
script: [{
type: 'application/ld+json',
children: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebPage",
"name": title,
"description": description,
"url": canonical
})
}]
})
</script>
<template>
<main class="wrap">
<h1>{{ title }}</h1>
<p>This page sets SEO meta, OG/Twitter, and JSON-LD.</p>
<p>Canonical: <code>{{ canonical }}</code></p>
</main>
</template>
π Breaking Down the SEO Implementation
1. Meta Tags with useSeoMeta()
The useSeoMeta()
composable is Nuxt 3's recommended way to handle meta tags:
useSeoMeta({
title, // <title> tag
description, // <meta name="description">
ogTitle: title, // <meta property="og:title">
ogDescription: description, // <meta property="og:description">
ogUrl: canonical, // <meta property="og:url">
ogType: 'website', // <meta property="og:type">
ogImage, // <meta property="og:image">
twitterCard: 'summary_large_image', // <meta name="twitter:card">
twitterTitle: title, // <meta name="twitter:title">
twitterDescription: description, // <meta name="twitter:description">
twitterImage: ogImage // <meta name="twitter:image">
})
2. Canonical URLs
Canonical URLs prevent duplicate content issues:
const canonical = new URL(route.fullPath || '/seo-demo', siteUrl).toString()
useHead({
link: [{ rel: 'canonical', href: canonical }]
})
3. JSON-LD Structured Data
Structured data helps search engines understand your content:
useHead({
script: [{
type: 'application/ld+json',
children: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebPage",
"name": title,
"description": description,
"url": canonical
})
}]
})
πΌοΈ Dynamic OG Image Generation
The magic happens in our API route (server/api/og.get.ts
):
import satori from 'satori'
import { Resvg } from '@resvg/resvg-js'
import { readFile } from 'node:fs/promises'
import { html } from 'satori-html'
export default defineEventHandler(async (event) => {
const q = getQuery(event)
const title = String(q.title ?? 'Nuxt SEO Demo')
// Load font from public/fonts directory
const font = await readFile('public/fonts/Inter-Regular.otf')
// Create HTML template using satori-html
const markup = html`<div style="
width:1200px;height:630px;display:flex;align-items:center;justify-content:center;
font-size:56px;font-family:Inter;padding:40px;background:#0ea5e9;color:#fff">
${title}
</div>`
// Generate SVG using Satori
const svg = await satori(markup, {
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: font, weight: 400, style: 'normal' }]
})
// Convert SVG to PNG using Resvg
const png = new Resvg(svg).render().asPng()
setHeader(event, 'content-type', 'image/png')
return png
})
How Dynamic OG Images Work
- URL Generation: The page creates an OG image URL with the title as a parameter
-
API Route:
/api/og
receives the title and generates a custom image - Satori Processing: Converts HTML/CSS to SVG
- Image Conversion: Resvg converts SVG to PNG
- Caching: Nuxt automatically caches the generated images
π¨ Font Setup
For custom fonts in OG images, place your font file in public/fonts/
:
public/
fonts/
Inter-Regular.otf
The font is loaded server-side and used by Satori for text rendering.
π§ Advanced SEO Patterns
Dynamic Meta Based on Content
// For blog posts or dynamic content
const { data: post } = await $fetch(`/api/posts/${route.params.slug}`)
useSeoMeta({
title: post.title,
description: post.excerpt,
ogImage: `/api/og?title=${encodeURIComponent(post.title)}&author=${post.author}`
})
Multiple JSON-LD Types
// Article with author information
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": description,
"author": {
"@type": "Person",
"name": "Your Name"
},
"datePublished": "2024-01-01",
"url": canonical
}
useHead({
script: [{ type: 'application/ld+json', children: JSON.stringify(jsonLd) }]
})
π Performance Considerations
Image Optimization
- Caching: Generated OG images are automatically cached by Nuxt
- Size: Standard 1200x630px for optimal social sharing
- Format: PNG for best compatibility across platforms
Server-Side Generation
- OG images are generated server-side, reducing client load
- Font loading happens once per build
- Satori provides excellent performance for SVG generation
π SEO Benefits
This implementation provides:
- Search Engine Optimization: Proper meta tags and structured data
- Social Media Sharing: Rich previews on Twitter, Facebook, LinkedIn
- Accessibility: Semantic HTML and proper heading structure
- Performance: Server-side generation and caching
- Flexibility: Dynamic content-based meta generation
π― Best Practices
Meta Tag Guidelines
- Title: Keep under 60 characters
- Description: 150-160 characters for optimal display
- OG Image: Always use absolute URLs
- Canonical: Prevent duplicate content penalties
Technical SEO
- Use semantic HTML structure
- Implement proper heading hierarchy (h1, h2, h3)
- Include alt attributes for images
- Ensure fast loading times
π Testing Your SEO
Tools to validate your implementation:
- Facebook Sharing Debugger: Test OG tags
- Twitter Card Validator: Verify Twitter cards
- Google Rich Results Test: Check structured data
- Chrome DevTools: Inspect generated meta tags
π Conclusion
This comprehensive SEO implementation demonstrates how Nuxt 3 makes it easy to create SEO-friendly applications. The combination of:
- useSeoMeta() for meta tag management
- Dynamic OG image generation with Satori
- JSON-LD structured data for rich snippets
- Canonical URLs for duplicate content prevention
Provides a solid foundation for any web application that needs excellent search engine visibility and social media sharing capabilities.
The server-side generation ensures fast performance while the dynamic nature allows for content-specific optimization. This approach scales well from simple marketing sites to complex web applications.
Top comments (0)