Problem
Editors need a place to work: forms, drafts, history, media. Building that yourself is a project of its own, and a hosted admin brings another service, another login and another place where the content lives.
Approach
The admin is a prebuilt React application in @krantho/admin, served by your app at /admin. It talks to the krantho API at /api/* and nothing else, so it works the same on Next.js, SvelteKit and Express. A SvelteKit site ships it without adding React to its own pages.
- Editor. A form generated from the collection's schema next to a markdown editor, with autosave, a lock that keeps two people from overwriting each other, and publish confirmation.
- Preview on the site. When the site enables it, an editor opens a draft on its real page. On Next.js this uses draft mode; on SvelteKit and Express a signed link that is valid for ten minutes and for that page only.
- Version history. Every save is a version, with a diff view and restore, plus checkpoints of the whole content store.
- Collections. Filter, sort and bulk-publish; see translation coverage per locale.
- Media and structure. A media library, and a Structure section with the site's Schema.org graph, its scopes and archetypes, rich results, and what crawlers and assistants read.
- An assistant, when you turn it on. A chat in Studio that finds, drafts and improves content and SEO on the language model you configure. It writes drafts, and asks you to confirm before it publishes, deletes or replaces anything.
- Sign-in. An access token, or OIDC with any provider that supports it.
/admincan send a visitor without a session straight to the provider and answer 404 when sign-in is refused.
Everything the admin does goes through the REST API, so the same operations are open to your own tools. The API describes itself in an OpenAPI 3.1 specification at /api/openapi.json, with an interactive reference at /api/docs. Plugins add endpoints under /api/plugins/<name>/, content hooks and field types. The krantho command-line tool, written in Go, works with content, schemas, checkpoints and SEO from the terminal through the same API.
Proof
The admin is one route in your app. On Next.js:
import { createAdminRouteHandler } from '@krantho/server-next'
export const { GET } = createAdminRouteHandler()SvelteKit uses createAdminHandler() from @krantho/server-sveltekit; Express mounts createAdminServer(config) from @krantho/server-node. To send visitors without a session straight to the OIDC provider, pass the site's configuration: createAdminRouteHandler({ config }).
import { createApiRouteHandlers } from '@krantho/server-next'
import config from '../../../krantho.config'
import { collections } from '../../../krantho.collections'
// The content API at /api/*: content, media, SEO, auth and the sitemap
export const { GET, POST, PUT, DELETE, PATCH } = createApiRouteHandlers({
config,
collections,
})
// The admin at /admin, in app/admin/[[...slug]]/route.ts:
//
// import { createAdminRouteHandler } from '@krantho/server-next'
// export const { GET } = createAdminRouteHandler()import { createApiHandlers } from '@krantho/server-sveltekit'
import config from '../../../../krantho.config'
import { collections } from '../../../../krantho.collections'
// The content API at /api/*: content, media, SEO, auth and the sitemap
export const { GET, POST, PUT, DELETE, PATCH } = createApiHandlers({
config,
collections,
// Editors preview drafts through a short-lived signed link
sitePreview: 'signed-url',
})
// The admin at /admin, in src/routes/admin/[...path]/+server.ts:
//
// import { createAdminHandler } from '@krantho/server-sveltekit'
// export const { GET } = createAdminHandler()import express from 'express'
import { createAdminServer, createHandler, toWebRequest, writeResponseHeaders } from '@krantho/server-node'
import config from './krantho.config'
import { collections } from './krantho.collections'
const api = createHandler({ config, collections, sitePreview: 'signed-url' })
const admin = createAdminServer(config)
const app = express()
async function send(res: express.Response, response: Response): Promise<void> {
res.status(response.status)
writeResponseHeaders(res, response)
res.end(Buffer.from(await response.arrayBuffer()))
}
const owns = (prefix: string, path: string) => path === prefix || path.startsWith(`${prefix}/`)
// Mounted without a path: the handler routes on the full URL
app.use(async (req, res, next) => {
if (owns('/api', req.path)) return send(res, await api.handle(await toWebRequest(req)))
if (!owns('/admin', req.path)) return next()
const url = new URL(`http://localhost${req.originalUrl}`)
return send(res, admin({ url, cookie: req.headers.cookie }))
})Limits
- Roles are admin, editor and viewer, and apply to the whole site. Permissions per collection are stored but not enforced yet.
- The admin's look is its own. It is not themed to your site.
- Editors change content, not layout. There is no visual page designer.