Build a single-page app that loads Birkly content as JSON and renders with your framework — not with {for each} inside JSX.
For React, Vue, and similar SPAs: call the Public API or Birkly.fetch / Birkly.getEntryBySlug, then render with components. Do not put Birkly template tags in JSX. Optional: load birkly-client.js and use Birkly.render(html, data) into a DOM ref when you want the string engine without Light DOM discovery.
Beginner
The rule
| Do | Don’t | |----|-------| | fetch JSON → map to <Article /> | {for each entry in 'blog'} inside JSX | | Birkly.getEntryBySlug('blog', slug) | {from 'blog' get entry …} as React children | | Configure CORS for your app origin | Expect processTemplates() to walk React’s virtual DOM |
Minimal React hook (Public API)
import { useEffect, useState } from 'react';
const CMS = 'https://your-cms.com';
export function useBlogEntry(slug) {
const [entry, setEntry] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const url =
`${CMS}/api/public.php?action=get_entry` +
`&collection=blog&slug=${encodeURIComponent(slug)}`;
fetch(url)
.then((r) => r.json())
.then((body) => {
if (cancelled) return;
if (body.status !== 'ok') throw new Error(body.message || 'API error');
setEntry(body.data?.entry ?? null);
})
.catch((e) => !cancelled && setError(e));
return () => { cancelled = true; };
}, [slug]);
return { entry, error };
}
export function BlogPost({ slug }) {
const { entry, error } = useBlogEntry(slug);
if (error) return <p>Could not load post.</p>;
if (!entry) return <p>Loading…</p>;
return (
<article>
<h1>{entry.title}</h1>
<div dangerouslySetInnerHTML={{ __html: entry.content || '' }} />
</article>
);
}
List with React (not a template loop)
function BlogIndex() {
const [entries, setEntries] = useState([]);
useEffect(() => {
fetch(`${CMS}/api/public.php?action=list_entries&collection=blog`)
.then((r) => r.json())
.then((body) => setEntries(body.data?.entries || []));
}, []);
return (
<ul>
{entries.map((e) => (
<li key={e.id || e.slug}>
<a href={`/blog/${e.slug}`}>{e.title}</a>
</li>
))}
</ul>
);
}
Optional: Birkly client helpers
<script src="https://your-cms.com/birkly-client.js"
data-cms-url="https://your-cms.com"></script>
// After the script loads:
const entry = await Birkly.getEntryBySlug('blog', 'my-post-slug');
const list = await Birkly.fetch('list_entries', { collection: 'blog' });
Optional: Birkly.render into a ref
When you have a template string (not JSX tags) and JSON data:
function CmsFragment({ template, data }) {
const ref = useRef(null);
useEffect(() => {
if (!ref.current || !window.Birkly?.render) return;
ref.current.innerHTML = Birkly.render(template, data);
}, [template, data]);
return <div ref={ref} />;
}
// template = "<h1>{title}</h1><article>{'content'}</article>"
// data = entry object from the API
Still do not write {for each} as JSX syntax — only as a string if you intentionally use the Birkly string engine.
Advanced Users
CORS
The browser origin of your SPA (e.g. https://app.example.com) must be allowed by Birkly / your reverse proxy. Failed CORS looks like a network error in DevTools, not a JSON body.
API shapes
- List:
GET /api/public.php?action=list_entries&collection=blog - One entry:
action=get_entry&collection=blog&slug=… - Batch:
POST /api/page_bundle.phpwith arequestsarray (see Client library / Public API)
Birkly.fetch(action, params) wraps the public endpoint. Prefer framework-owned state for routing and caching; use the client when you already load birkly-client.js.
Why not Light DOM processTemplates in SPA roots?
processTemplates discovers DOM regions with template text. React/Vue continuously reconcile their trees — mixing Birkly innerHTML replacement with virtual DOM usually fights the framework. Keep Birkly tags on static HTML pages; keep SPAs on JSON.
Vue sketch
import { ref, onMounted } from 'vue';
export function useEntries(collection) {
const entries = ref([]);
onMounted(async () => {
const res = await fetch(
`https://your-cms.com/api/public.php?action=list_entries&collection=${collection}`
);
const body = await res.json();
entries.value = body.data?.entries || [];
});
return { entries };
}