Integrations
Use gluso with Astro
Astro's build-time frontmatter is a natural fit for a headless CMS: fetch published entries while building, ship zero JavaScript. Examples assume a collection whose API name is posts with a slug field — swap in your own collection's API name.
1. Environment variables
Create a content.read key in Settings → Space → Developers. Without the PUBLIC_ prefix, Astro keeps these server/build-only:
# .env GLUSO_API_KEY=glk_…
2. A fetch helper
// src/lib/gluso.ts
const BASE = 'https://glu.so'
export async function gluso(path, query = {}) {
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 ' + globalThis._importMeta_.env.GLUSO_API_KEY },
})
if (!res.ok) throw new Error('gluso ' + res.status + ': ' + url.pathname)
return res.json()
}3. List & detail pages
---
// src/pages/blog/index.astro
import { gluso } from '../../lib/gluso'
const { list } = await gluso('posts', { perPage: '20' })
---
<ul>
{list.map((post) => (
<li><a href={'/blog/' + post.slug}>{post.data.title}</a></li>
))}
</ul>Rich-text fields are HTML. Sanitize before set:html unless every author is trusted — a tiny build-time helper:
// src/lib/sanitize.ts import sanitizeHtml from 'sanitize-html' // npm i sanitize-html export const sanitize = (html) => sanitizeHtml(html ?? '')
---
// src/pages/blog/[slug].astro
import { gluso } from '../../lib/gluso'
import { sanitize } from '../../lib/sanitize'
export async function getStaticPaths() {
const { list } = await gluso('posts')
return list.map((post) => ({ params: { slug: post.slug } }))
}
const { slug } = Astro.params
const { doc } = await gluso('posts/' + slug, { depth: '1' })
---
<article>
<h1>{doc.data.title}</h1>
<div set:html={sanitize(doc.data.body)} />
</article>Static builds snapshot content at build time; rebuild on publish (a deploy hook) or switch the route to SSR for always-fresh content. Field values under data are keyed by API name — for hyphenated names use bracket access (data['related-posts']).
4. Personalization (optional)
Static pages can't vary per visitor at build time, so fetch the raw override layer + audience catalog (no variants param) and resolve in a small client island — or use SSR/edge rendering and pass ?variants= per request. Details in API → Personalization.
5. Live preview (optional)
Set the collection's preview baseUrl to your Astro site (path template /blog/{slug}) in its settings, then enable it from the entry editor's ⋮ (More actions) → Visual editing.
The editor streams draft edits over postMessage — inherently client-side, so give the elements ids and add a script tag that updates them from each message when ?preview=1 is present. No framework needed:
<!-- add ids to the fields you want live, then this script -->
<article>
<h1 id="post-title">{doc.data.title}</h1>
<div id="post-body" set:html={sanitize(doc.data.body)} />
</article>
<script>
const GLUSO_ORIGIN = 'https://your-gluso-app.com'
if (new URLSearchParams(location.search).has('preview')) {
window.addEventListener('message', (event) => {
if (event.origin !== GLUSO_ORIGIN) return
if (event.data?.type !== 'gluso:preview') return
const data = event.data.data ?? {}
document.getElementById('post-title').textContent = data.title ?? ''
document.getElementById('post-body').innerHTML = data.body ?? '' // sanitize in prod
})
window.parent.postMessage({ type: 'gluso:preview-ready' }, GLUSO_ORIGIN)
}
</script>Astro bundles page script tags into a module by default — that's fine here. See the visual editing guide for the full protocol (variant-resolved data arrives the same way).