Static sites via Pull-API (Starlight, Astro, Hugo …)
Orimora can be the content source for a static site without a Git mirror in between. The site pulls the published documents from Orimora’s token-secured Pull-API at build time and renders them — Orimora stays the single source of truth, and there is no separate content repository to maintain.
This works with any generator that reads Markdown + frontmatter (Astro/Starlight, Hugo, Jekyll, Eleventy, …). The example uses Starlight.
How it works
Section titled “How it works”- You publish documents (or a folder) to a channel in Orimora — only those are exposed, nothing else (no draft leak).
- A small pull script runs before each site build, fetches the published set from the Pull-API, and writes one Markdown file per document.
- The generator builds as usual from those files.
First: SSG or SSR? Two very different paths
Section titled “First: SSG or SSR? Two very different paths”This recipe is for static generators (SSG: Starlight, Hugo, Jekyll, Eleventy …) — they only pick up content at build time, which is why a pull script and a rebuild trigger are needed.
Server-rendered apps (SSR/ISR — Next.js, Nuxt, SvelteKit …) don’t need this recipe.
They simply call the Pull API at request time (with a cache in front) — no build,
no hook, publishing is live immediately. The API supports ETag / If-None-Match
(304 responses) and updatedSince for incremental fetches.
1. Create the channel in Orimora
Section titled “1. Create the channel in Orimora”- Settings → Publishing → New channel, transport Pull, format Markdown. Optional: add your host’s deploy hook URL — Orimora then triggers the rebuild automatically after every publish (details).
- Publish your documents to it via “Send to channel…” on a document or folder — this runs on your login session and needs no token.
- Open the channel and create a token — treat it like a password. The token is for the consumer only (the pull script), not for publishing.
You now have three values for the script: the Orimora base URL, the channel id,
and the token. Base URL and channel id are part of the Pull API URL shown on
the channel card, ready to copy
(https://<base-url>/api/v1/published/<channel-id>/documents).
Verify immediately
Section titled “Verify immediately”Before building anything, test the chain with curl (copy the pull URL from the channel card, insert the token):
curl -H "Authorization: Bearer <token>" \ "https://wiki.example.com/api/v1/published/<channel-id>/documents"You must see your published documents as JSON (data: [...]). A 401 means the
token is wrong; a 403 means the token belongs to a different channel; an empty
data means nothing was sent to this channel yet. From now on the channel stamps
“Last pulled” — your permanent signal that the build really fetches.
2. The pull script
Section titled “2. The pull script”Drop this into your site repo as scripts/pull-orimora.mjs. It pages through the
Pull-API (cursor-based) and writes src/content/docs/orimora/<slug>.md:
import { mkdir, rm, writeFile } from 'node:fs/promises';import { join } from 'node:path';
const BASE = process.env.ORIMORA_URL; // e.g. https://wiki.example.comconst CHANNEL = process.env.ORIMORA_CHANNEL; // channel idconst TOKEN = process.env.ORIMORA_TOKEN; // channel tokenconst OUT = 'src/content/docs/orimora'; // target directory
if (!BASE || !CHANNEL || !TOKEN) { throw new Error('Set ORIMORA_URL, ORIMORA_CHANNEL and ORIMORA_TOKEN');}
async function pull() { const docs = []; let cursor = null; do { const url = new URL(`${BASE}/api/v1/published/${CHANNEL}/documents`); url.searchParams.set('limit', '200'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } }); if (!res.ok) throw new Error(`Pull failed: ${res.status} ${res.statusText}`); const { data, meta } = await res.json(); docs.push(...data); cursor = meta?.nextCursor ?? null; } while (cursor); return docs;}
function toMarkdownFile(doc) { // `content` is already Markdown because the channel format is "markdown". // `frontmatter` carries title + any fields mapped on the channel. const fm = { title: doc.title, ...doc.frontmatter }; const yaml = Object.entries(fm) .filter(([, v]) => v !== null && v !== undefined) .map(([k, v]) => `${k}: ${JSON.stringify(v)}`) .join('\n'); return `---\n${yaml}\n---\n\n${doc.content ?? ''}`;}
const docs = await pull();await rm(OUT, { recursive: true, force: true });for (const doc of docs) { // Group into human-readable folders by collection name (not opaque ids). const dir = join(OUT, doc.collectionSlug ?? 'uncategorised'); await mkdir(dir, { recursive: true }); await writeFile(join(dir, `${doc.slug}.md`), toMarkdownFile(doc), 'utf-8');}console.log(`Pulled ${docs.length} document(s) from Orimora into ${OUT}`);3. Run it before the build
Section titled “3. Run it before the build”Wire it as a prebuild step so every build refreshes content:
{ "scripts": { "prebuild": "node scripts/pull-orimora.mjs", "build": "astro build" }}npm run build now pulls first, then builds. On a host like Netlify/Vercel/Cloudflare
Pages, set ORIMORA_URL, ORIMORA_CHANNEL, ORIMORA_TOKEN as build environment
variables.
4. Trigger the rebuild automatically
Section titled “4. Trigger the rebuild automatically”Publishing only writes to Orimora — your site shows the content after the next build. To automate that, add your host’s deploy hook URL to the pull channel (edit channel → configuration): Orimora then POSTs to it after every publish. Where to find the URL and which platforms are covered is in the publishing guide. Without a hook you’re left with manual, scheduled, or git-triggered rebuilds — the publish dialog reminds you (“trigger your build now”).
Pull-API reference
Section titled “Pull-API reference”GET /api/v1/published/{channelId}/documents — Authorization: Bearer <token>.
| Query | Meaning |
|---|---|
limit | 1–200 (default 50) |
cursor | ISO timestamp of the last item — cursor pagination |
updatedSince | ISO timestamp — only documents changed after this point |
Each item: id, title, emoji, slug, collectionId, collectionName,
collectionSlug (ready as a folder segment), updatedAt, publishedAt,
content (Markdown for a markdown channel), frontmatter. meta.nextCursor is
null on the last page.
Case study: these docs dogfood themselves
Section titled “Case study: these docs dogfood themselves”The Praxistipps section of these very docs is pulled live from Orimora using exactly this recipe — it’s the feature’s reality check:
- The tips are written in Orimora and published to a pull channel (“Starlight Doku”).
docs/scripts/pull-orimora.mjsruns before everyastro buildand fetches the set via the Pull API. Env vars:ORIMORA_URL,ORIMORA_PRAXISTIPPS_CHANNEL,ORIMORA_PRAXISTIPPS_TOKEN— set in Coolify on the docs app and passed through as Docker build args (see the caution above).- Deliberately robust: if the variables are missing or Orimora is unreachable, the script skips with exit 0 — an Orimora outage can never break the docs build; the committed placeholder index stays in place.
- Verification: the build log prints
[pull-orimora] Pulled N Praxistipp(s)(or the skip reason), and the channel shows “Last pulled” with the build timestamp.
See also
Section titled “See also”- Publishing channels — channels, tokens, transports, deploy hook
- REST API overview — auth, rate limits, pagination