Integrations
Use gluso with Cloudflare Workers
Run Astro as an edge-rendered Worker and fetch published entries per request — new content is live instantly (no rebuild) and pages can vary per visitor. The one thing that changes from a static build: on Workers there is no process.env at load time, so the API key comes from the runtime env, not globalThis._importMeta_.env. Examples assume a collection whose API name is posts with a slug field.
1. Adapter & config
Add the Cloudflare adapter and render on the server. platformProxy runs the Cloudflare runtime locally under astro dev, loading vars/secrets from .dev.vars.
// astro.config.mjs
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
export default defineConfig({
output: 'server',
adapter: cloudflare({ platformProxy: { enabled: true } }),
})Astro's edge runtime and HTML sanitizers use Node built-ins, so enable nodejs_compat. The key is a secret — keep it out of wrangler.toml:
# wrangler.toml name = "my-gluso-site" main = "./dist/_worker.js/index.js" compatibility_date = "2025-05-05" compatibility_flags = ["nodejs_compat"] [assets] directory = "./dist" binding = "ASSETS" [vars] GLUSO_BASE_URL = "https://glu.so" GLUSO_COLLECTION = "posts" # GLUSO_API_KEY is a secret — see below, never inline it here.
2. The API key
Create a content.read key in Settings → Space → Developers. For local dev, drop it in .dev.vars (gitignored); for production, store it as a Worker secret:
# .dev.vars (local only, gitignored) GLUSO_API_KEY=glk_… # production wrangler secret put GLUSO_API_KEY
3. A runtime-aware fetch helper
Build the client from the per-request env you read via Astro.locals.runtime.env — not module-level globalThis._importMeta_.env, which is empty for secrets on Workers.
// src/lib/gluso.ts
// On Workers, secrets arrive per-request via the runtime env — build the
// client from the env, don't read globalThis._importMeta_.env at module load.
export function createGluso(env) {
const BASE = env.GLUSO_BASE_URL
return 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, String(v))
const res = await fetch(url, {
headers: { Authorization: 'Bearer ' + env.GLUSO_API_KEY },
})
if (!res.ok) throw new Error('gluso ' + res.status + ': ' + url.pathname)
return res.json()
}
}4. List & detail pages
Every request renders in the Worker, so there's no getStaticPaths and no rebuild — publish and it's live.
---
// src/pages/blog/index.astro — edge SSR, fresh every request.
import { createGluso } from '../../lib/gluso'
const gluso = createGluso(Astro.locals.runtime.env)
const { list } = await gluso('posts', { perPage: 20 })
---
<ul>
{list.map((post) => (
<li><a href={'/blog/' + post.slug}>{post.data.title}</a></li>
))}
</ul>---
// src/pages/blog/[slug].astro — no getStaticPaths; new entries are live instantly.
import { createGluso } from '../../lib/gluso'
import sanitizeHtml from 'sanitize-html' // npm i sanitize-html
const gluso = createGluso(Astro.locals.runtime.env)
const { slug } = Astro.params
const { doc } = await gluso('posts/' + slug, { depth: 1 })
if (!doc) return new Response('Not found', { status: 404 })
---
<article>
<h1>{doc.data.title}</h1>
<div set:html={sanitizeHtml(doc.data.body ?? '')} />
</article>Rich-text fields are HTML — sanitize before set:html unless every author is trusted (sanitize-html works with nodejs_compat). Field values under data are keyed by API name; use bracket access for hyphenated names (data['related-posts']).
5. Component & section pages
Landing pages are usually assembled from components — a hero, stacked sections, a panel. In gluso these are component fields: a single component, or an array of them. Each value is { componentTypeId, data }, where data holds that component's fields keyed by API name:
// GET /api/v1/public/content/pages/home?depth=1 → doc.data
{
"title": "Home",
"heroSection": { // a single component field
"componentTypeId": "6a3847…",
"data": { "heading": "Ship to the edge", "ctaUrl": "/signup" }
},
"sections": [ // an array of component fields
{ "componentTypeId": "6a38af…", "data": { "heading": "Model once" } },
{ "componentTypeId": "6a38af…", "data": { "heading": "Personalize" } }
]
}Render them through a registry that maps each componentTypeId to an Astro component (find the id under the component type's settings, or an entry's API tab):
---
// src/components/Section.astro — map componentTypeId → an Astro component.
import Hero from './Hero.astro'
import FeatureBlock from './FeatureBlock.astro'
const REGISTRY = {
'6a3847…': Hero, // your Hero component type id
'6a38af…': FeatureBlock, // your Feature Block component type id
}
const { block } = Astro.props
const Cmp = block ? REGISTRY[block.componentTypeId] : null // unknown type → skip
---
{Cmp && <Cmp data={block.data} />}A page then composes the fields — a single hero, an array of stacked sections. Because every request renders in the Worker, adding or reordering sections in the editor is live on the next request — no rebuild:
---
// src/pages/[slug].astro — edge SSR, no getStaticPaths.
import { createGluso } from '../lib/gluso'
import Section from '../components/Section.astro'
const gluso = createGluso(Astro.locals.runtime.env)
const { slug } = Astro.params
const { doc } = await gluso('pages/' + slug, { depth: 1 })
if (!doc) return new Response('Not found', { status: 404 })
const page = doc.data
---
<Section block={page.heroSection} />
{(page.sections ?? []).map((block) => <Section block={block} />)}6. Personalization at the edge (optional)
Because pages render per request, you can resolve variants server-side from any edge signal: the visitor's country/device (request.cf / cf-ipcountry) and first-party cookies (a returning-visitor flag, a saved segment). Map each signal to a variant id and forward them via ?variants= so the API returns already-resolved values. With the space's fallback on, multiple active ids stack (geo and cookie):
---
// Map edge signals — geo + a visitor cookie — to variant ids, then let the API
// resolve them. With the space's fallback on, both ids stack.
import { createGluso } from '../lib/gluso'
const gluso = createGluso(Astro.locals.runtime.env)
// geo: real cf-ipcountry on Workers
const country = Astro.request.headers.get('cf-ipcountry') ?? ''
// cookie: recognise (and remember) a returning visitor
const returning = Astro.cookies.get('gluso_visitor')?.value === '1'
Astro.cookies.set('gluso_visitor', '1', { path: '/', maxAge: 31536000 })
const ids = []
if (country === 'NG') ids.push('ng')
if (returning) ids.push('returning')
const { doc } = await gluso('pages/home', { depth: 1, variants: ids.join(',') })
---Full rules in API → Personalization. The mapping (signal → variant id) is yours; gluso only defines the audience catalog and applies the overrides. A cookie-varied response is per-visitor — set Cache-Control: private (or Vary: Cookie) so a shared CDN cache never serves one visitor's variant to another.
7. Live editing (optional)
Point the collection's preview baseUrl at your Worker (or http://localhost:4700 in dev) with a path template like /p/{slug}, then open an entry's ⋮ (More actions) → Visual editing. The editor loads that URL with ?preview=1 and streams the entry's draft data (variants resolved) over postMessage on every keystroke.
Edge SSR still renders the published page normally; a small island updates it live when ?preview=1 is present. Tag each editable node with a data-gluso path into the entry data — nested component/section fields included — and one framework-agnostic listener re-applies them:
<!-- Tag editable nodes with a data-gluso path into the entry data -->
<h1 data-gluso="heroSection.data.heading">{page.heroSection.data.heading}</h1>
<script is:inline define:vars={{ GLUSO_ORIGIN: 'https://your-gluso-app.com' }}>
if (new URLSearchParams(location.search).has('preview')) {
// Read a value out of the entry data by path (supports [n] array indices).
const get = (o, p) => p.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean)
.reduce((a, k) => (a == null ? a : a[k]), o)
window.addEventListener('message', (e) => {
if (e.origin !== GLUSO_ORIGIN || e.data?.type !== 'gluso:preview') return
const data = e.data.data ?? {}
document.querySelectorAll('[data-gluso]').forEach((el) => {
el.textContent = get(data, el.getAttribute('data-gluso')) ?? ''
})
})
window.parent.postMessage({ type: 'gluso:preview-ready' }, GLUSO_ORIGIN)
}
</script>Full protocol in Visual editing. Because the whole entry's draft data streams each message, editing any nested component field updates its node instantly.
8. Run & deploy with Wrangler
astro dev gives you the Cloudflare runtime via platformProxy; to exercise the real Worker under workerd, build then run wrangler dev.
# dev — Cloudflare runtime locally (loads .dev.vars) npx astro dev # build + run the real Worker under workerd npx astro build npx wrangler dev # deploy npx wrangler secret put GLUSO_API_KEY npx astro build && npx wrangler deploy