DrystoneCMS

Integrations

Works with the front end
you are already using.

Drystone renders no pages; it delivers content over an ordinary REST API. What you put in front is your choice — and you can revise that choice later without touching your content.

Next.js React Vue & Nuxt SvelteKit Astro Any other language

Pick your framework.

Every example below is the shortest one that works: fetch, render, and the one thing that differs in production.

App Router · Server Components

Next.js

Fetch content in a Server Component. The request runs on the server, so your delivery key never reaches the browser. With `revalidate` Next caches the response; with a webhook on publish you refresh precisely instead of on a timer.

  • Webhook on publish → a route handler that calls revalidateTag("articles")
  • generateStaticParams() with the same call for static detail pages
  • Put a preview token in a cookie and serve drafts inside draftMode()

app/blog/page.tsx

const API = process.env.DRYSTONE_API_URL;

async function getArticles() {
  const res = await fetch(`${API}/api/v1/content/articles?sort=-published_at&limit=10`, {
    headers: { Authorization: `Bearer ${process.env.DRYSTONE_API_KEY}` },
    // Time-based revalidation, or keep it and call revalidateTag() from your webhook.
    next: { revalidate: 300, tags: ["articles"] },
  });
  if (!res.ok) throw new Error(`Drystone: HTTP ${res.status}`);
  const { data } = await res.json();
  return data;
}

export default async function BlogPage() {
  const articles = await getArticles();
  return (
    <ul>
      {articles.map((a) => (
        <li key={a.id}><a href={`/blog/${a.slug}`}>{a.title}</a></li>
      ))}
    </ul>
  );
}

Vite · client-side

React

In a single-page app you fetch from the browser. Watch one thing: anything the browser sends is readable by the visitor, so never put a management key there. Use a delivery key with read scope only — or, if the content is not public, a small proxy on your own server.

  • Read-scoped keys only in the client; management keys stay server-side
  • Content that is not public? Put a proxy in between that adds the key
  • TanStack Query works just as well — it is a plain fetch

src/useArticles.ts

import useSWR from "swr";

const API = import.meta.env.VITE_DRYSTONE_API_URL;
const KEY = import.meta.env.VITE_DRYSTONE_API_KEY; // read-scoped, and therefore public

const fetcher = async (path: string) => {
  const res = await fetch(API + path, { headers: { Authorization: `Bearer ${KEY}` } });
  if (!res.ok) throw new Error(`Drystone: HTTP ${res.status}`);
  return (await res.json()).data;
};

export function useArticles() {
  // ETag and Cache-Control sit on every GET, so the browser reuses the response itself.
  return useSWR("/api/v1/content/articles?sort=-published_at&limit=10", fetcher);
}

Nuxt 3 · useFetch

Vue & Nuxt

In Nuxt you fetch with useFetch, which runs on the server during SSR and hands the result to the client — one call instead of two. Building statically with nuxt generate does the same thing at build time. For a plain Vue SPA the React rule applies: read-scoped keys only.

  • runtimeConfig without the public prefix keeps the key server-side
  • nuxt generate plus a webhook on publish = a static site that rebuilds itself
  • Vue without Nuxt: the same fetch in a composable, with a read-scoped key

pages/blog.vue

<script setup lang="ts">
const config = useRuntimeConfig();

const { data: articles, error } = await useFetch("/api/v1/content/articles", {
  baseURL: config.drystoneApiUrl,
  headers: { Authorization: `Bearer ${config.drystoneApiKey}` },
  query: { sort: "-published_at", limit: 10 },
  transform: (res) => res.data,
});
</script>

<template>
  <p v-if="error">Could not load articles.</p>
  <ul v-else>
    <li v-for="a in articles" :key="a.id">
      <NuxtLink :to="`/blog/${a.slug}`">{{ a.title }}</NuxtLink>
    </li>
  </ul>
</template>

load · server-side

SvelteKit

Put the call in a +page.server.ts. It runs on the server only, so your key stays there, and the result arrives as props in your component. With prerender on, the same code produces a static page.

  • $env/dynamic/private is server-only — the key cannot leak into the bundle
  • entries() alongside prerender for a static page per slug
  • A webhook on publish that kicks off your deploy

src/routes/blog/+page.server.ts

import { env } from "$env/dynamic/private";
import type { PageServerLoad } from "./$types";

export const prerender = true; // build statically; drop it for SSR on every request

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch(
    `${env.DRYSTONE_API_URL}/api/v1/content/articles?sort=-published_at&limit=10`,
    { headers: { Authorization: `Bearer ${env.DRYSTONE_API_KEY}` } },
  );
  if (!res.ok) throw new Error(`Drystone: HTTP ${res.status}`);
  const { data } = await res.json();
  return { articles: data };
};

Build-time · zero client JS

Astro

Astro fetches content while building and writes HTML. No CMS sits in your visitor's request path — the fastest option there is, and exactly how this project's own marketing site works.

  • getStaticPaths() with the same call for a page per article
  • Webhook on publish → rebuild at your host; live within a minute
  • A complete working example lives in examples/astro-blog in the repository

src/pages/blog.astro

---
const API = import.meta.env.DRYSTONE_API_URL;

const res = await fetch(`${API}/api/v1/content/articles?sort=-published_at&limit=10`, {
  headers: { Authorization: `Bearer ${import.meta.env.DRYSTONE_API_KEY}` },
});
const { data: articles } = await res.json();
---

<ul>
  {articles.map((a) => (
    <li><a href={`/blog/${a.slug}`}>{a.title}</a></li>
  ))}
</ul>

REST · OpenAPI 3

Any other language

There is no SDK to install and no query language to learn: it is REST with JSON. Python, Go, PHP, Ruby, a shell script in CI — anything that speaks HTTP can join. The OpenAPI 3 spec is served live at /api/docs, so generating a client is an option too.

  • Pagination, filtering, sorting and field selection live in the query string
  • ETag and Cache-Control on every GET — send If-None-Match and get a 304
  • Generate a client from the OpenAPI spec in the language of your choice

Terminal

# Published articles, filtered and sorted
curl -H "Authorization: Bearer $DRYSTONE_API_KEY" \
  "$DRYSTONE_API_URL/api/v1/content/articles?filter[category]=news&sort=-published_at&limit=10"

# A single document, with its relation loaded
curl -H "Authorization: Bearer $DRYSTONE_API_KEY" \
  "$DRYSTONE_API_URL/api/v1/content/articles/my-slug?populate=author"

# The OpenAPI spec, for example to generate a client from
curl "$DRYSTONE_API_URL/api/docs/openapi.json" -o openapi.json

And then in production

Three things you set up once.

Webhooks for rebuilds

On publish, change and delete, Drystone POSTs to the URL you configure. Static site: trigger a rebuild. Next.js or Nuxt: refresh exactly the cache entries that changed.

Preview before publishing

The preview endpoints serve drafts when given a token. Set that token server-side in a cookie and your editor sees their draft in the real front end, side by side with the editor.

Types from your schema

drystone types > types.ts generates typings that match your content model exactly. No manual upkeep, and renaming a field becomes a compile error instead of undefined in production.

Frequently asked

Two questions that come first.

Does Drystone CMS work with Next.js, React or Vue?

Yes. Drystone delivers content over an ordinary REST API with JSON, so any framework that speaks HTTP works — Next.js, React, Vue, Nuxt, SvelteKit, Astro, or a language without a framework. There is no SDK to install and no query language to learn.

May the API key live in my front end?

A delivery key with read scope may live in a server-side environment. Inside a browser bundle any key is public: that is fine for content that is public anyway, and not acceptable for the rest — put a proxy or server-side rendering in front of those. Management keys never belong in a front end.

One rule about API keys

A delivery key with read scope may live in a server-side environment; inside a browser bundle any key is public by definition. That is fine for content that is public anyway, and not acceptable for the rest — put a proxy or server-side rendering in front of those. Management keys never belong in a front end, in any framework.

Get started Compare with your current CMS