Nuxt
Render your Opinly blog in Nuxt 3 — server-side fetch with useAsyncData, Vue rendering, and SEO via useHead.
In Nuxt you fetch content server-side with useAsyncData, render it with @opinly/vue, and
drive SEO with @opinly/nuxt (which shapes a useHead() payload).
Install
pnpm add @opinly/backend @opinly/vue @opinly/nuxt @opinly/sharednpm i @opinly/backend @opinly/vue @opinly/nuxt @opinly/sharedyarn add @opinly/backend @opinly/vue @opinly/nuxt @opinly/sharedAdd your API key to the environment (OPINLY_API_KEY). The fetch runs on the server, so the key
stays private.
Fetch, render, and add SEO
<script setup lang="ts">
import { createOpinlyClient } from '@opinly/backend'
import type { OpinlyNode } from '@opinly/shared'
import { OpinlyContent } from '@opinly/vue'
import { opinlyHead, buildBlogPostingJsonLd } from '@opinly/nuxt'
const route = useRoute()
const config = {
imagesPrefix: '/images',
siteUrl: 'https://example.com',
blogPrefix: '/blog',
siteName: 'Acme',
}
// Server-side fetch (runs during SSR). This renders the single-post route; for an
// index, category archive, or author route, branch on the slug and call
// posts()/categories()/author() instead.
const { data: post } = await useAsyncData('opinly', async () => {
const opinly = createOpinlyClient({ apiKey: process.env.OPINLY_API_KEY })
const slug = route.params.slug as string // flat, single-segment post slug
return opinly.post(slug) // FullPost | null
})
// SEO → Nuxt useHead (title, OG tags, JSON-LD).
watchEffect(() => {
if (post.value) {
useHead(
opinlyHead({
resolved: { type: 'post', data: post.value },
config,
jsonLd: [buildBlogPostingJsonLd(post.value, config)],
}),
)
}
})
</script>
<template>
<main v-if="post">
<h1>{{ post.title }}</h1>
<OpinlyContent :content="post.content as OpinlyNode" :config="config" />
</main>
</template>opinlyHead({ resolved, config, jsonLd? }) returns a plain object with title, meta, and
script entries — exactly the shape useHead() expects. @opinly/nuxt also re-exports the
JSON-LD builders (buildBlogPostingJsonLd, buildFaqJsonLd, …) so you don't need to import
@opinly/shared separately for those.
Notes
<OpinlyContent>renders Vue vnodes via Vue'sh()— no React anywhere.- Style the rendered body with your own classes or the
classNamesprop — see Rendering.
Images
Point imagesPrefix straight at the CDN — images load directly, no proxy or build config:
const config = {
imagesPrefix: 'https://cdn.opinly.ai/REPLACE-ME-xxxxxxxx', // your 21-char CDN namespace
// …siteUrl, blogPrefix, siteName
}For automatic resizing/format optimization, add @nuxt/image and render
with <NuxtImg> (it has a built-in CDN image provider). Opinly serves images by fileKey, so the
absolute URL works in dev and production without a same-origin rewrite.