Integrations
Use gluso with Next.js
gluso is plain HTTP + JSON, so Next.js needs no SDK — a small fetch helper and your App Router pages are done. Examples assume a collection whose API name is posts with a slug field — swap in your own collection's API name (shown in the entry editor's API tab).
1. Environment variables
Create an API key with the content.read scope (Settings → Space → Developers) and keep it server-side — no NEXT_PUBLIC_ prefix:
# .env.local GLUSO_API_KEY=glk_…
2. A fetch helper
// lib/gluso.ts
const BASE = 'https://glu.so'
export async function gluso<T>(
path: string,
query: Record<string, string> = {},
): Promise<T> {
const url = new URL('/api/v1/public/content/' + path, BASE)
for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v)
const res = await fetch(url, {
headers: { Authorization: 'Bearer ' + process.env.GLUSO_API_KEY },
next: { revalidate: 60 },
})
if (!res.ok) throw new Error('gluso ' + res.status + ': ' + url.pathname)
return res.json()
}next.revalidate gives you ISR — pages re-fetch at most once a minute. Use cache: 'no-store' instead for always-fresh data.
3. List & detail pages
// app/blog/page.tsx
import { gluso } from '@/lib/gluso'
export default async function BlogIndex() {
const { list } = await gluso<{ list: any[] }>('posts', { perPage: '20' })
return (
<ul>
{list.map((post) => (
<li key={post.id}>
<a href={'/blog/' + post.slug}>{post.data.title}</a>
</li>
))}
</ul>
)
}Rich-text fields come back as HTML strings. Sanitize before rendering unless every entry author is trusted — a tiny helper (works in server components):
// lib/sanitize.ts
import DOMPurify from 'isomorphic-dompurify' // npm i isomorphic-dompurify
export function sanitize(html: string): string {
return DOMPurify.sanitize(html ?? '')
}The detail page fetches by slug and renders the sanitized body:
// app/blog/[slug]/page.tsx
import { gluso } from '@/lib/gluso'
import { sanitize } from '@/lib/sanitize'
export async function generateStaticParams() {
const { list } = await gluso<{ list: any[] }>('posts')
return list.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost(props: {
params: Promise<{ slug: string }>
}) {
const { slug } = await props.params
const { doc } = await gluso<{ doc: any }>('posts/' + slug, { depth: '1' })
return (
<article>
<h1>{doc.data.title}</h1>
<div dangerouslySetInnerHTML={{ __html: sanitize(doc.data.body) }} />
</article>
)
}related-posts), dot access won't parse — use bracket access: data['related-posts'].4. Personalization (optional)
Decide the visitor's variants in middleware or the page (from geo headers, device, UTM params, cookies) and pass them along — gluso merges the overrides server-side:
// e.g. derive variants from the request, then:
const { doc } = await gluso('posts/' + slug, {
variants: ['mobile', 'returning'].join(','),
})
// doc.data is already merged for that audienceCareful with ISR: personalized responses vary per visitor, so either resolve variants in middleware and cache per combination, or fetch the raw layer once and resolve client-side — see API → Personalization.
5. Live preview (optional)
Point the collection's preview baseUrl at your Next.js site (e.g. pathTemplate: /blog/{slug}) in its settings. The editor then loads your page in an iframe and streams draft edits over postMessage — the visual editing guide covers the protocol and the usePreview hook.
App Router pages are server components, and hooks only run on the client — so pass the server-fetched data into a small 'use client' component that calls usePreview. It renders published data normally, and swaps to live draft data (variant-resolved) while the editor drives it:
// app/blog/[slug]/PreviewablePost.tsx
'use client'
import { usePreview } from '@/lib/usePreview'
import { sanitize } from '@/lib/sanitize'
export default function PreviewablePost({ published }: { published: any }) {
const data = usePreview(published) // published data, or live draft in preview
return (
<article>
<h1>{data.title}</h1>
<div dangerouslySetInnerHTML={{ __html: sanitize(data.body) }} />
</article>
)
}Then the detail page just fetches and hands off to it:
// app/blog/[slug]/page.tsx
import { gluso } from '@/lib/gluso'
import PreviewablePost from './PreviewablePost'
export default async function BlogPost(props: {
params: Promise<{ slug: string }>
}) {
const { slug } = await props.params
const { doc } = await gluso<{ doc: any }>('posts/' + slug, { depth: '1' })
return <PreviewablePost published={doc.data} />
}Enable it from the entry editor: ⋮ (More actions) → Visual editing.
6. Component & section pages
Landing pages are often built from components — a hero, stacked content sections, a sidebar panel. In gluso these are component fields: a single component, or an array of them. Each value is { componentTypeId, data }, where data is that component's fields keyed by API name:
// GET /pages/home?depth=1 → doc.data
{
"title": "Home",
"heroSection": { // a single component field
"componentTypeId": "6a3847…",
"data": { "heading": "Build fast", "ctaUrl": "/signup" }
},
"sections": [ // an array of component fields
{ "componentTypeId": "6a3847…", "data": { "heading": "Model any content" } },
{ "componentTypeId": "6a3847…", "data": { "heading": "Edit visually" } }
],
"featuresPanel": { "componentTypeId": "6a38af…", "data": { "features": [] } }
}Render them with a registry that maps each componentTypeId to a React component (find the id under the component type's settings, or the entry's API tab):
// components/sections/registry.tsx
import Hero from './Hero'
import FeaturesPanel from './FeaturesPanel'
const REGISTRY: Record<string, React.ComponentType<{ data: any }>> = {
'6a3847…': Hero, // your Hero component type id
'6a38af…': FeaturesPanel, // your Features Panel component type id
}
export function Section({ block }: { block?: { componentTypeId: string; data: any } }) {
if (!block) return null
const Cmp = REGISTRY[block.componentTypeId]
return Cmp ? <Cmp data={block.data} /> : null // unknown type → skip
}Then a page composes the fields — a single hero, an array of stacked sections, a sidebar panel — and live editing just works: the same usePreview hook streams the whole page's draft data, so editing any nested component field re-renders its section instantly.
// app/[slug]/PreviewablePage.tsx
'use client'
import { usePreview } from '@/lib/usePreview'
import { Section } from '@/components/sections/registry'
export default function PreviewablePage({ published }: { published: any }) {
const data = usePreview(published)
if (!data) return null
return (
<div className="layout">
<Section block={data.heroSection} />
<div className="grid">
<main>
{(data.sections ?? []).map((block: any, i: number) => (
<Section key={i} block={block} />
))}
</main>
<aside><Section block={data.featuresPanel} /></aside>
</div>
</div>
)
}