Integrations
Use gluso with Nuxt
Keep the API key in server-only runtime config, proxy gluso through a Nitro route, and consume it with useFetch. Examples assume a collection whose API name is posts with a slug field — swap in your own collection's API name.
1. Runtime config
Create a content.read key in Settings → Space → Developers. Non-public runtime config never reaches the browser:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
glusoApiKey: '', // NUXT_GLUSO_API_KEY
},
})2. A server proxy route
A catch-all Nitro route forwards content reads and attaches the key server-side — pages and components never see it:
// server/api/content/[...path].get.ts
export default defineEventHandler((event) => {
const { glusoApiKey } = useRuntimeConfig()
const path = getRouterParam(event, 'path') || ''
// Allowlist collection/slug segments so a caller can't traverse out of the
// content prefix and reach other endpoints with the server-side key.
if (!/^[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)*$/.test(path)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid content path' })
}
return $fetch('/api/v1/public/content/' + path, {
baseURL: 'https://glu.so',
query: getQuery(event),
headers: { Authorization: 'Bearer ' + glusoApiKey },
})
})3. List & detail pages
<!-- pages/blog/index.vue -->
<script setup>
const { data } = await useFetch('/api/content/posts', {
query: { perPage: 20 },
})
</script>
<template>
<ul>
<li v-for="post in data?.list" :key="post.id">
<NuxtLink :to="'/blog/' + post.slug">{{ post.data.title }}</NuxtLink>
</li>
</ul>
</template>Rich-text fields are HTML. v-html renders on the server and client, so sanitize with an isomorphic helper unless every author is trusted:
// plugins/sanitize.ts — npm i isomorphic-dompurify
import DOMPurify from 'isomorphic-dompurify'
export default defineNuxtPlugin(() => ({
provide: { sanitize: (html) => DOMPurify.sanitize(html ?? '') },
}))<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute()
const { $sanitize } = useNuxtApp()
const { data } = await useFetch(
'/api/content/posts/' + route.params.slug,
{ query: { depth: 1 } },
)
</script>
<template>
<article v-if="data?.doc">
<h1>{{ data.doc.data.title }}</h1>
<div v-html="$sanitize(data.doc.data.body)" />
</article>
</template>useFetch SSRs the request and hydrates without a second fetch; add routeRules caching (SWR/ISR) on top for CDN-friendly pages. Field values under data are keyed by API name — hyphenated names need bracket access (data['related-posts']).
4. Personalization (optional)
Derive the visitor's variants in the proxy route (device from user-agent, UTM from the query, segments from cookies) and forward them — the response comes back merged:
// in the proxy route, before $fetch:
const variants = []
if (/mobile/i.test(getHeader(event, 'user-agent') || '')) variants.push('mobile')
if (getCookie(event, 'returning')) variants.push('returning')
return $fetch('/api/v1/public/content/' + path, {
baseURL: 'https://glu.so',
query: { ...getQuery(event), variants: variants.join(',') },
headers: { Authorization: 'Bearer ' + glusoApiKey },
})Personalized responses vary per visitor — skip shared-cache routeRules on those routes, or resolve client-side from the raw layer instead. See API → Personalization.
5. Live preview (optional)
Set the collection's preview baseUrl to your Nuxt site (path template /blog/{slug}) in its settings, then enable it from the entry editor's ⋮ (More actions) → Visual editing.
In the detail page, register the bridge listener in onMounted and let a ref override the fetched data while previewing — the template renders from a computed that prefers the draft:
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute()
const { $sanitize } = useNuxtApp()
const { data } = await useFetch(
'/api/content/posts/' + route.params.slug,
{ query: { depth: 1 } },
)
const GLUSO_ORIGIN = 'https://your-gluso-app.com'
const preview = ref(null)
const view = computed(() => preview.value ?? data.value?.doc?.data ?? null)
onMounted(() => {
if (!new URLSearchParams(location.search).has('preview')) return
const onMessage = (event) => {
if (event.origin !== GLUSO_ORIGIN) return
if (event.data?.type === 'gluso:preview') preview.value = event.data.data
}
window.addEventListener('message', onMessage)
window.parent.postMessage({ type: 'gluso:preview-ready' }, GLUSO_ORIGIN)
onUnmounted(() => window.removeEventListener('message', onMessage))
})
</script>
<template>
<article v-if="view">
<h1>{{ view.title }}</h1>
<div v-html="$sanitize(view.body)" />
</article>
</template>Variant-resolved data arrives the same way — flipping personalization in the editor re-renders the preview. Full protocol in the visual editing guide.