Use Your Own Frontend (Headless)

You do not have to use the SpiderPublish site renderer to publish with SpiderPublish. Keep writing your blog posts, docs, press releases and changelog in SpiderPublish, and build the website yourself — a Next.js or React app on your own Cloudflare Worker, Vercel, or any host, or a mobile app. Your frontend reads the published content over the public content API and renders it with your own design.

  You write in SpiderPublish              Your own site (your code, your design)
  (dashboard, MCP, agents)                Next.js / React / mobile app
            │ Publish                               │
            ▼                                       │  GET /content/posts
   SpiderPublish content API  ◄─────────────────────┘  X-Content-Domain: yoursite.com
            │
            └────────────── JSON ──────────────────►  your pages

What you can read

Everything published on the site, with no API key:

Content

Endpoints

Blog

GET /content/posts, /content/posts/{slug}, /content/posts/featured, /content/posts/search?q=

Authors, tags, categories

GET /content/authors, /content/authors/{slug}, /content/tags, /content/categories

Docs

GET /content/docs/tree, /content/docs/{path}, /content/docs/search?q=

Press

GET /content/press, /content/press/{slug}, /content/press/contacts, /content/press/kits, /content/press/kits/{slug}, /content/press/boilerplate

Feeds

GET /content/feed.xml, /content/atom.xml, /content/feed.json (blog) and /content/press/feed.xml, /content/press/atom.xml, /content/press/feed.json (press)

Changelog

GET /content/changelog, /content/changelog/{version}

Pages

GET /content/pages, /content/pages/{slug} — blocks as data; your frontend designs each block type

Navigation and settings

GET /content/navigation/{location}, /content/settings, /content/sitemap

Custom collections

GET /content/data-sources/{collection}/items, /content/data-sources/{collection}/items/{record}

The base URL is https://app.spideriq.ai/api/v1. Drafts, scheduled and embargoed items are never returned.

Tell the API which site you are

Send the X-Content-Domain header on every request. Its value is a verified domain of your site (yoursite.com) or your site's platform host (<id>.sites.spideriq.ai).

curl -H "X-Content-Domain: yoursite.com" \
  "https://app.spideriq.ai/api/v1/content/posts?page_size=10"

If the header names a domain SpiderPublish does not know, the API answers 404 with No content configured for domain: …. That is a configuration error, not an empty blog.

If your workspace runs more than one site, use a verified domain of the site you mean: a platform host without its own verified domain reads the whole workspace.

A feed reader cannot send headers, so the feed URLs also take the site as ?domain=:

https://app.spideriq.ai/api/v1/content/press/feed.xml?domain=yoursite.com

The header wins when you send both. A ?domain= that is not a verified domain of a site answers 404, the same as a wrong header. Only the feeds read ?domain=; every other route needs the header.

Rendering article bodies

Posts, docs pages, press releases and changelog entries come with two copies of the body:

  • body — the stored rich-text document (Tiptap JSON), if you want to render it yourself.

  • body_html — the same body rendered to HTML by SpiderPublish. Text and attributes are escaped, and link and image addresses are limited to http(s), mailto, tel and relative paths, so you can insert it into your page directly.

body_html is on single-item reads. List endpoints return summaries (the changelog list also carries each entry's body, without body_html).

A marketplace component placed inside an article arrives in body_html as an empty placeholder you can replace with your own component:

<div data-spiderpublish-component="pricing-table" data-component-props="{&quot;plan&quot;:&quot;pro&quot;}"></div>

The SDK: @spideriq/publish-sdk

The SDK wraps all of the above for JavaScript and TypeScript: typed readers, the site header, retries, timeouts, null for a missing item, and cache tags. It uses only fetch, so it runs in Cloudflare Workers.

The package is on the SpiderIQ registry, not on npmjs.com. Point the @spideriq scope at it once, in your project's .npmrc:

@spideriq:registry=https://npm.spideriq.ai

Then install the pinned version:

npm install @spideriq/publish-sdk@0.1.0
// lib/cms.ts
import { createSpiderPublish } from "@spideriq/publish-sdk";
import { nextCache } from "@spideriq/publish-sdk/next";

export const cms = createSpiderPublish({
  site: "yoursite.com",
  requestInit: nextCache({ revalidate: 300 }),
});
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { PostBody } from "@spideriq/publish-sdk/react";
import { cms } from "@/lib/cms";

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await cms.posts.get(slug);
  if (!post) notFound();
  return (
    <article>
      <h1>{post.title}</h1>
      <PostBody html={post.body_html} className="prose" />
    </article>
  );
}

The other readers follow the same shape: cms.docs.tree(), cms.docs.get("guides/install"), cms.press.list(), cms.changelog.get("v2.9.0"), cms.navigation("header"), cms.collection("case-studies").list().

A newsroom from public reads

Everything a press page shows comes from public reads:

On the page

Read

The list of releases

cms.press.list()

One release, with its contacts, its "About" block and its media kit

cms.press.get(slug): contacts, boilerplate, media_kit

The media kits, default first

cms.press.kits(), then cms.press.kit(slug) for one kit's files

The "About" block on the newsroom page

cms.press.boilerplate(), or cms.press.boilerplate({ language: "de" })

Press contacts

cms.press.contacts()

A release's media_kit is the kit you attached to that release, or the site's default kit when you attached none. It is null when the site has no kits. Link each file to its download_url so downloads are counted. boilerplate() returns null when the site has no default "About" block.

Map component placeholders to your own components with <PostBody components={{ "pricing-table": Pricing }} />. Outside React, splitBodyHtml(html) gives you the HTML and component segments.

Caching

Cache reads on your side — GET /content/posts/{slug} also counts a page view, so an uncached site counts every render. With Next.js, nextCache() stores each read in the Next.js data cache under tags such as spiderpublish:yoursite.com:post:my-post. Evict one item, or everything for the site:

import { revalidateTag } from "next/cache";
import { cacheTags } from "@spideriq/publish-sdk/next";

revalidateTag(cacheTags.post("yoursite.com", "my-post"));
revalidateTag(cacheTags.site("yoursite.com"));

Published changes appear once your cache expires — five minutes with the default above.

Calling from the browser

Calls from your server — Server Components, route handlers, a Worker — work from any host.

Calls from browser code on your domain (a client-side docs search, a "load more" button) work for reads once that domain is a verified domain of your SpiderPublish site — see Custom Domains. The browser sends no cookies and only the Content-Type, Accept and X-Content-Domain headers.

Writes and metered calls are not open to browsers on other domains: ask the docs (POST /content/docs/ask), semantic search, the API playground, booking submit and slot holds, funnel events and the press newsletter signup. Relay those through a route on your own server, which calls the API with the same X-Content-Domain header.

Funnels keep their step state in a cookie on the hosted site, so link to a funnel's hosted page (/f/<id>) rather than rebuilding it in your frontend.

Newsroom subscriber pages

Every newsroom email carries two links for the journalist: confirm (in the sign-up email) and unsubscribe (in every release and in the List-Unsubscribe header that Gmail and Yahoo show as an unsubscribe button). Mail whose links all point at someone else's domain looks more like spam to mailbox providers, so a site with its own frontend can host both pages itself.

Serve two routes on your site:

Your route

On GET

On POST

/press/confirm?token=…

Show a page with a Confirm button that posts back to this URL. Do not confirm on GET — link scanners in mail systems open every link.

Call POST /content/press/subscribe/confirm?token=…

/press/unsubscribe?token=…

Show a page with an Unsubscribe button that posts back to this URL.

Call POST /content/press/unsubscribe?token=…

Pass the token through unchanged and send your X-Content-Domain header. Send Accept: application/json to get a JSON answer — {"ok": true, "state": "confirmed"} or {"ok": true, "state": "unsubscribed"}; any token problem answers 200 with "state": "invalid".

The unsubscribe POST must work with no page in between: mailbox providers send it straight to the List-Unsubscribe address (the one-click unsubscribe of RFC 8058, body List-Unsubscribe=One-Click). Relay that POST to the API as it comes and answer 200 once the API has answered 200. If the API returns an error, return an error too — a 200 tells the provider the unsubscribe worked when it did not.

When both routes are live, set extensions.press.public_base_url to your site's address (see Newsroom). From then on, release, confirm and unsubscribe links in your newsroom emails point at your site.

Next steps

Publish