Guides

Use Pluto headless

Read the content of Pluto from a site in any framework or stack.

Pluto is a first-class part of a Nuxt app, but your site does not have to use Nuxt. The admin and the API of Pluto run in a Nuxt app. A site in any framework or language reads the published content from the Public API.

You get two apps:

  • The Pluto app. It has the admin at /admin and the public API. Create it with npm create pluto@latest -t admin (the Dashboard only template: no public site), and deploy it to a host with a server runtime. See Deploy to Vercel.
  • Your site. Any framework or stack. It reads the content from the Pluto app.

Read the public API

The same routes work in both modes, GitHub and Supabase. They are anonymous, they return published items only, and they answer JSON.

Any JavaScript runtime
const cms = 'https://cms.example.com'

// The published posts, newest first, without the body.
const list = await fetch(`${cms}/api/pluto/v1/content/post?limit=10&sort=-publishedAt`).then(res => res.json())
// list = { data: [{ id, title, slug, excerpt, createdAt, ... }], meta: { total, limit, offset } }

// One post, with its body as sanitized HTML.
const post = await fetch(`${cms}/api/pluto/v1/content/post/hello-world`).then(res => res.json())
// post = { data: { id, title, slug, html, ... } }

The query (limit, offset, search, sort, filter[field]), the answers and the errors are in Public API.

Call it from the browser

The API sends Access-Control-Allow-Origin: * by default, so browser code on another origin can call it. To allow only your site, set the origins in the nuxt.config.ts of the Pluto app:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    plutoApi: { cors: ['https://www.example.com'] },
  },
})

Cache

A success answer has Cache-Control: public, s-maxage=60. A CDN in front of the Pluto app keeps it for 60 seconds. Change it with runtimeConfig.plutoApi.maxAge.

Read the Markdown files (GitHub mode)

In GitHub mode, the content is Markdown with YAML frontmatter in your GitHub repository, for example content/posts/hello-world.md. A site that builds from that repository can also read the files at build time, with any static site generator.

  • Each save in the admin is a commit. The commit messages have a [skip ci] marker by default. Set plutoGithub.commit.skipDeploy: false so that your host builds the site after each save. See Commits.
  • The frontmatter has a status key. Leave out the files with status: draft.

Read Supabase directly

With the Supabase layer, the content is in your Supabase project. Your site can also read it with a Supabase client library, or the REST API of Supabase, with the anon key. Row level security gives an anonymous caller published rows only. See Grants.

Any JavaScript runtime
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
const { data: posts } = await supabase.from('posts').select('*').order('created_at', { ascending: false })

The public API is easier in most cases: it gives html for a post, and the media URLs, the category and the availability for a product.

What stays in Nuxt

  • The admin, the sign-in, and the extension points. A layer, a field widget, or a settings page is Vue and Nuxt code in the Pluto app.
  • The composables, such as usePlutoPublicList, and the components, such as PlutoHtml. Your site calls the routes and renders the content with its own code.
  • The admin bar with the New and Edit buttons shows only on the pages of the Pluto app.