Build your site

Authors

Give a post one or more authors on both backends, read them from the public API, and build the author pages of a site.

A post can have one or more authors. This works on both backends (the Git CMS and Supabase), on a server and on a static site. Readers get a byline, an author box and a page for each author: /authors/<slug>.

An author is a content type. A post stores the ids of its authors in a reference field with multiple. The order of the list is the order of the byline. The author pages are the public API with a filter.

The blog template has the author pages already.

The authors

Each backend registers the content type author. An author has these fields:

FieldTypeUse
nametextThe title field. Shown as Name.
slugslugMade from the name.
biotextareaA short text about the author.
avatarmediaAn image. The picker accepts images only.

An author has no status. An author is public when you save it.

An author has no link to a user account. So no email address and no role go to the public site. A guest author or a pen name is a normal author.

Git CMSSupabase
Where an author livescontent/authors/<slug>.mdThe table authors
Where a post keeps its authorsA YAML list of slugs in the front matterThe column posts.author_ids (bigint[])
Id of an authorThe slug (the file name)The number id of the row
Set upNothing. The type registers with postType.Run the migration 008_post_authors.sql of @plutocms/supabase-blog

Git CMS

An author is a Markdown file with front matter only. A post lists the slugs of its authors:

content/authors/ana-lima.md
---
name: Ana Lima
bio: Ana writes about trails and weather.
avatar: ana-lima-k3j9a
---
content/posts/a-weekend-in-the-hills.md
---
title: A weekend in the hills
authors:
  - ana-lima
---
  • The file has name, bio, avatar and the timestamps.
  • The type registers when plutoGithub.postType is on, which is the default. With postType: false, the type is not there.
  • A rename of an author (a new slug) rewrites every post that uses it, in the same commit as the rename.
  • Remove moves an author to the trash. The posts keep the author. Delete forever, or the purge after 30 days, removes the author from the posts, in the same commit. If the list is then empty, Pluto removes the authors key.
  • If a post changes while the commit runs, the save answers 409.

Supabase

Migration 008_post_authors.sql of @plutocms/supabase-blog adds:

  • the table authors, with the columns id, created_at, updated_at, name, slug (unique), bio and avatar;
  • the column author_ids on posts, a bigint array in byline order;
  • the policies, the grants and a trigger. When an author is deleted, the trigger removes its id from every post.

Remove moves an author to the trash (migration 009_trash.sql). The posts keep its id. Delete forever, or the purge after 30 days, deletes the row, and the trigger runs.

Anyone can read the authors that are not in the trash. A write needs authors:manage.

Apply the migration from /admin/migrations. A post save fails until the migration runs. See Migrations.

Who can manage authors

Creating, editing and deleting an author need the authors:manage capability.

  • Git CMS: every user with push access has it.
  • Supabase: when it runs, migration 008 gives authors:manage to each role that holds posts:publish. Give it by hand to a role that you add later. An admin holds all capabilities.

A user without authors:manage can still pick the existing authors in a post.

In the admin

The admin menu has an Authors item. It opens the list of authors and the form of an author.

The post form

The side panel of the post form has the field Authors. It is a multi-select with a search box. A selected author is a chip. The order of the chips is the order of the byline.

To add an author that does not exist, type the name and select Create "…". Pluto creates the author with the name and a slug made from it, then selects it. The option shows only for a user with authors:manage.

The field has remember: true. A new post gets the authors that the user saved last time. Pluto keeps this list for each browser.

  • An author that does not exist anymore is not filled.
  • A new device fills nothing until the first save there.

Read authors in the public API

The author is a public type, so it has its own routes:

RouteAnswer
GET /api/pluto/v1/content/authorThe authors.
GET /api/pluto/v1/content/author/<slug>One author.

An author is { id, name, slug, bio, avatar, createdAt, updatedAt }. avatar is a media record, or null.

A post answer has its authors in place of the ids. Each author is { id, label, slug, avatar, bio }:

{
  "id": "a-weekend-in-the-hills",
  "title": "A weekend in the hills",
  "authors": [
    {
      "id": "ana-lima",
      "label": "Ana Lima",
      "slug": "ana-lima",
      "avatar": { "id": "ana-lima-k3j9a", "url": "/media/ana-lima.webp", "width": 400, "height": 400, "alt": "" },
      "bio": "Ana writes about trails and weather."
    }
  ]
}
  • A post with no author has [].
  • An author that does not exist anymore is not in the answer.
  • avatar and bio come from the option publicFields of the field. See Reference fields.

To list the posts of an author, filter by the slug or by the id:

Terminal
curl "https://your-site.dev/api/pluto/v1/content/post?filter[authors]=ana-lima"

A slug or an id that matches no author gives an empty list. The filter works on a static site too. See Public API.

Author pages in a Nuxt site

Two composables read the authors: useAuthors and useAuthor. useAuthors sorts by name. A post of usePosts has authors.

app/pages/authors/[slug].vue
<script setup lang="ts">
const route = useRoute()
const { author } = await useAuthor(() => String(route.params.slug))
const { posts } = await usePosts({
  filter: { authors: String(route.params.slug) }
})
</script>

<template>
  <h1>{{ author?.name }}</h1>
  <p>{{ author?.bio }}</p>
  <PostList :posts="posts" />
</template>
  • useAuthors(options) returns authors.
  • useAuthor(slug) returns author. A missing author shows the 404 page.
  • The types are PlutoAuthor, PlutoPostAuthor and PlutoPostSummary.authors.

The blog template has these pages:

PageContent
/authors/<slug>An author with the avatar, the bio and the posts.
/posts/<slug>A byline under the title, and an author box after the text.

An author has a View link in the admin. Its previewUrl is /authors/<slug>. If your site uses another path, change it with extendContentTypes. See Extend a content type of another layer.

shared/utils/author-paths.ts
export const authorPathsExtension: PlutoContentTypeExtension = {
  id: 'author-paths',
  extend: type =>
    type.name === 'author'
      ? { ...type, previewUrl: item => (item.slug ? `/team/${item.slug}` : undefined) }
      : type
}

Register it on the client with definePlutoExtension and on the server with extendContentTypes, as in the guide above.

The authors field

The post type has this field:

{
  name: 'authors',
  type: 'reference',
  label: 'Authors',
  target: 'author',
  multiple: true,
  create: true,
  remember: true,
  publicFields: ['avatar', 'bio'],
  region: 'side'
}
  • create creates an author from the field. The user needs authors:manage.
  • remember fills a new post with the last saved value.
  • publicFields adds avatar and bio to each author in the public API.

SEO

With the SEO add-on, the pages get the author tags: <meta name="author">, article:author, JSON-LD and <dc:creator> in the feed.

Limits

  • An author has no link to a user account.
  • The post form loads up to 1000 authors. A blog with more authors needs a server search, which Pluto does not have yet.
  • On the Git CMS, a rename reads every post file. On a large blog, one commit writes many files.
  • On GitHub, an inline create is its own commit and its own build. The next save can cancel the build before it ends.