Use Pluto headless
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
/adminand the public API. Create it withnpm 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.
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:
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. SetplutoGithub.commit.skipDeploy: falseso that your host builds the site after each save. See Commits. - The frontmatter has a
statuskey. Leave out the files withstatus: 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.
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 asPlutoHtml. 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.