Public read
The public site reads published posts through the Public API of core. The composables and the component are for a Nuxt site. A site in any other framework or stack calls the routes. See Use Pluto headless.
A public read is anonymous by design. The answer is the same for every visitor. Drafts never show, not even to a signed-in editor. The response never sets a cookie.
Routes
The post type of the CMS is public: api: { public: true }.
GET /api/pluto/v1/content/postreturns the published posts:{ data, meta: { total, limit, offset } }. A list item has no body. It hasexcerpt: theexcerptfield when it has text, else a plain text version of the body (200 characters).GET /api/pluto/v1/content/post/<slug>returns one published post:{ data }. It has the Markdown body andhtml, the body rendered and sanitized.- The list takes
limit(1 to 100, default 20),offset,search,sort(for example-publishedAt), andfilter[<field>]. - A draft or a missing file gives 404. A nested type works too: the id can hold slashes.
- A success answer has
Cache-Control: public, s-maxage=60, stale-while-revalidate=300. Set the time withruntimeConfig.plutoApi.maxAge. An error answer has no public cache header.
Your own content types work the same way when they set api: { public: true }. See Public API.
Composables
Both composables wrap useFetch. They run on the server, so the page HTML holds the data.
<script setup lang="ts">
const { data } = await usePlutoPublicList<{ id: string, title: string, excerpt: string }>('post', { limit: 10 })
</script>
<template>
<article v-for="post in data?.data" :key="post.id">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
</template>
<script setup lang="ts">
const route = useRoute()
const { data } = await usePlutoPublicItem<{ title: string, html: string }>('post', String(route.params.slug))
</script>
<template>
<PlutoHtml v-if="data" :html="data.data.html" />
</template>
usePlutoPublicList(type, { limit, offset, search, sort, filter })gives the list answer.usePlutoPublicItem(type, slug)gives the item answer, withhtml. A draft or a missing slug puts a 404 inerror.
PlutoHtml
PlutoHtml is a core component. It shows the sanitized html of a public item, from the Git CMS or from any other backend (for example @plutocms/supabase-blog), so a page works with both.
| Prop | Default | Meaning |
|---|---|---|
html | none | The html of a public item |
tag | article | The wrapper element |
class | none | Extra classes for the wrapper |
The component sets the HTML as innerHTML. It never parses Markdown on the client. Pass only the html of a public item to it.
PlutoMarkdown is the older name, for the Git CMS only. It renders PlutoHtml.
How the Markdown renders
Pluto renders Markdown with marked and GFM. Raw HTML in the Markdown never reaches the output. Then a sanitizer runs on the result.
The rendered HTML is cached by blob id. A blob id never changes, so the entry never goes stale.
See Security for the sanitizer rules.
Offset your header for the admin bar
On a public page, the admin bar shows for staff only. It is fixed at the top of the page. Core sets the token --pluto-admin-bar-height (2.5rem) on :root. While the bar shows, PlutoRoot sets data-pluto-admin-bar on <html>. When the server renders the bar (see The bar in the first paint), the attribute is in the server HTML too, so the header does not move.
Use them to move a fixed header below the bar:
:root[data-pluto-admin-bar] .site-header {
top: var(--pluto-admin-bar-height);
}
In script, usePlutoAdminBar().visible tells if the bar shows. See Admin shell.
Show "Edit Post" in the admin bar
The post type shows New Post in the admin bar. To show Edit Post on a post page, tell core which post the page shows:
const route = useRoute()
usePlutoEditLink('post', () => String(route.params.slug))
The function also returns the edit link for staff, and null for a visitor. See New and Edit buttons.