Skip to content

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 of a shared folder 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.

  1. You create a folder share for a collection in Orimora. It mints a read-only, published-only token scoped to exactly that folder — only its published documents are ever exposed (no draft leak).
  2. A small pull script runs before each site build, fetches the published set from the Pull-API as Markdown, and writes one Markdown file per document.
  3. 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 updatedSince for incremental fetches.

  1. In the sidebar, open a collection’s menu (kebab or right-click) and choose “Connect to a service…”. (Requires the collection.share capability — Admin-only by default.)
  2. Pick the target type Static site (SSG) and create the share. Orimora mints a read token and shows it once — copy it now and store it like a password. The dialog also shows the API endpoint.
  3. Note the collection id (it’s in the collection URL) — the pull script filters on it.

You now have three values for the script: the Orimora base URL, the collection id, and the token.

Before building anything, test the chain with curl:

Terminal window
curl -H "Authorization: Bearer <token>" \
"https://wiki.example.com/api/v1/documents?collectionId=<collection-id>&format=markdown"

You must see the folder’s published documents as JSON (data: [...]), each with a markdownText body and a slug. A 401 means the token is wrong; a 403 means the token isn’t scoped to this collection; an empty data means the folder has no published documents yet (drafts never appear).

Drop this into your site repo as scripts/pull-orimora.mjs. It pages through the Pull-API (offset-based) and writes src/content/docs/orimora/<slug>.md:

scripts/pull-orimora.mjs
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const BASE = process.env.ORIMORA_URL; // e.g. https://wiki.example.com
const COLLECTION = process.env.ORIMORA_COLLECTION; // collection id
const TOKEN = process.env.ORIMORA_TOKEN; // folder-share token (kb_…)
const OUT = 'src/content/docs/orimora'; // target directory
const PAGE = 200;
if (!BASE || !COLLECTION || !TOKEN) {
throw new Error('Set ORIMORA_URL, ORIMORA_COLLECTION and ORIMORA_TOKEN');
}
async function pull() {
const docs = [];
let offset = 0;
let total = Infinity;
do {
const url = new URL(`${BASE}/api/v1/documents`);
url.searchParams.set('collectionId', COLLECTION);
url.searchParams.set('format', 'markdown');
url.searchParams.set('limit', String(PAGE));
url.searchParams.set('offset', String(offset));
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!res.ok) throw new Error(`Pull failed: ${res.status} ${res.statusText}`);
const { data, total: t } = await res.json();
docs.push(...data);
total = typeof t === 'number' ? t : docs.length;
offset += PAGE;
} while (docs.length < total && offset < total);
return docs;
}
function toMarkdownFile(doc) {
// `markdownText` is the serialized body (?format=markdown). Only the title
// travels as frontmatter — Starlight's docsSchema requires it.
const fm = { title: doc.title };
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.markdownText ?? ''}`;
}
const docs = await pull();
await rm(OUT, { recursive: true, force: true });
await mkdir(OUT, { recursive: true });
for (const doc of docs) {
await writeFile(join(OUT, `${doc.slug}.md`), toMarkdownFile(doc), 'utf-8');
}
console.log(`Pulled ${docs.length} document(s) from Orimora into ${OUT}`);

Wire it as a prebuild step so every build refreshes content:

package.json
{
"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_COLLECTION, ORIMORA_TOKEN as build environment variables.

Publishing only writes to Orimora — a static site shows the content after the next build. Trigger it from your host: a scheduled build, a manual deploy, or a git push. (Automatic rebuild-on-publish for folder shares is on the roadmap.) For SSR/ISR apps the question doesn’t arise — they read the Pull-API live.

GET /api/v1/documentsAuthorization: Bearer <token>.

QueryMeaning
collectionIdFilter to one folder (the share’s collection)
formatmarkdown → add a markdownText body + a derived slug
limit1–100 (default 25)
offsetOffset pagination; the response carries total
updatedSinceISO timestamp — only documents changed after this point

Each item: id, title, emoji, collectionId, collectionName, collectionSlug, updatedAt, publishedAt, tags, and — with ?format=markdownslug and markdownText. A published-only share token returns only published documents regardless of any status parameter.

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 live in a Praxistipps collection in Orimora, exposed via a folder share (read-only, published-only).
  • docs/scripts/pull-orimora.mjs runs before every astro build and fetches the set via the Pull API. Env vars: ORIMORA_URL, ORIMORA_PRAXISTIPPS_COLLECTION, 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).
  • REST API overview — auth, rate limits, pagination
  • WordPress — import the same folder into WordPress instead
  • Kirby — the same pull pattern for Kirby
  • Git mirror — push a folder to a Git repo (vs. the site pulling)