Show your space's posts on your own site
A common use of the DFOS API: render your public space's posts on a site you control — a marketing page, a blog, a landing page. This recipe walks the whole path. It assumes your space has a public profile turned on.
1. Fetch the space (optional)
If you want your space's name, avatar, or links in the header, fetch the space once:
curl https://api.dfos.com/v1/spaces/field-notes
Address it by subdomain, entity id, or protocol DID. Store the id or did,
not the subdomain — the subdomain is a mutable alias. See
Spaces.
2. List the public posts
curl 'https://api.dfos.com/v1/spaces/field-notes/posts?limit=20'
Posts come back newest first. Each item gives you a displayTitle (already
handles untitled posts), an excerpt, a cover, counts, isPinned, and
publishedAt. If you want pinned posts at the top, sort on isPinned yourself
— the feed does not reorder them. Page with nextCursor (see
Conventions).
3. Link each post — or fetch its body
The listing does not include post bodies. You have two options:
- Link out. Fetch each post and link to its
canonicalUri— the canonical public permalink, which respects the space's custom domain. Cheapest path; DFOS renders the post. - Render inline. Fetch the single post and render its
bodyyourself when it is readable.
Fetch a single post by its id (not its slug):
curl https://api.dfos.com/v1/spaces/field-notes/posts/post_4k8m2q7v
The response is a union on state. Handle both arms:
async function renderPost(space, postId) {
const res = await fetch(`https://api.dfos.com/v1/spaces/${space}/posts/${postId}`);
if (res.status === 404) return null; // missing, private, or wrong space
const data = await res.json();
if (data.state === 'eligible') {
const post = data.post;
// post.folded === true means only the above-fold teaser is present;
// link to post.canonicalUri for the rest.
return { title: post.displayTitle, body: post.body, href: post.canonicalUri };
}
// data.state === 'gated' — no body; show a join / subscribe CTA.
// Treat an unrecognized data.reason as 'restricted'.
return { title: data.space.displayName, cta: data.reason, join: data.space.domain };
}
Two things to respect:
- The gated arm carries no content — render the
spaceCTA (itsdomain,joinMode, andsubscribeEnabledtell you how to prompt a join or subscribe), never a fake preview. folded === truemeans the body is truncated at a fold. Show the teaser and link tocanonicalUri; there is no anonymous way to fetch below-fold content.
4. Cache and stay compatible
- Fetch fresh; don't blind-cache. Data responses are served
Cache-Control: no-store— the API does not want you sharing a cache of them, because a post response can carry inline, time-limited signed media URLs. Fetch a resource when you need it, and watch theRateLimit-*headers and the429retry signal so you stay inside your budget. If you proxy the API from your own server, pass the responses through — do not introduce your own shared cache of data responses. See Rate limits and caching. - Tolerate growth. Ignore fields you don't recognize, and treat every enum
(
joinMode,siteMode, the gatedreason) as open — new values can appear without a version bump. See the compatibility contract. - Store stable ids. Key your own storage on
id/did, never on subdomains, handles, or slugs.
See Posts and Spaces for the full field semantics, and the interactive reference to try every endpoint.