The Dashboard
One route, the whole Dashboard
/uncial/ is the Dashboard: the signed-in area where an editor runs the whole site. It is shipped by uncial-cms and works the same on the github, local and server forges. It has four kinds of section:
Pages lists the site's Content documents with search, filters and paging, creates and deletes them, and opens each one for editing.
Media browses, uploads and deletes the site's media. See Media sources.
Globals edits site-wide data, such as menus or a banner, as forms. See Globals.
App sections are the host's own screens, slotted into the frame or linked from its nav. See App sections.
The Dashboard is a custom element, <uncial-dashboard>, so any framework can host it. As with the rest of uncial-cms, the package registers no route: you write the one route that renders it.
SvelteKit
Importing uncial-cms/dashboard registers the element. Its options are objects and functions, which a prerendered attribute cannot carry, so render the element after mount and let Svelte set them as properties:
// src/routes/uncial/+page.server.ts
import { createIndexHandlers } from 'uncial-cms/sveltekit';
import { blocks, schema, site } from '../site.js';
const handlers = createIndexHandlers({ site, blocks, schema });
export const load = handlers.load;<!-- src/routes/uncial/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { base } from '$app/paths';
import 'uncial-cms/dashboard';
import { blocks, schema, site } from '../site.js';
let mounted = $state(false);
onMount(() => (mounted = true));
</script>
{#if mounted}
<uncial-dashboard config={site} {blocks} {schema} basePath={base}></uncial-dashboard>
{/if}config takes the whole site object from defineSite, so the Dashboard also finds the site's Globals there. blocks and schema are the ones the route factories take, and schema may be a per-path resolver here too. A Svelte host can instead import the Dashboard component from uncial-cms/svelte and pass the same options as props. Slotted App sections are the exception: they need the custom element, whose shadow root they project into.
Astro
An Astro page renders the element and sets its options from a script. Astro bundles the script for this page alone, so no reader page gains CMS JavaScript:
---
// src/pages/uncial.astro
---
<html lang="en">
<head><title>Dashboard</title></head>
<body>
<uncial-dashboard></uncial-dashboard>
<script>
import 'uncial-cms/dashboard';
import { defineSite } from 'uncial-cms';
import { blocks, schema } from '../site';
import { siteOptions } from '../site-options';
Object.assign(document.querySelector('uncial-dashboard')!, {
config: defineSite(siteOptions),
blocks,
schema
});
</script>
</body>
</html>The element waits for config before it signs in, so setting its options after it has upgraded is safe. The same pattern works on a plain HTML page with a <script type="module">.
Options
Every option is a property of the element, or a prop of the Svelte component:
configis required: the site object fromdefineSiteordefineServerSite, or a bare site config.blocksandschemaare required, as the route factories take them.globalsnames the site's Globals and their schemas. It defaults to the site object'sglobals.editorHref(pagePath)opens a page in the host's own Editor variant. Without it, a page opens in the Fallback editor at#/edit/<path>. A static site usually leaves it out, because a page created in the Dashboard has no Editor variant until the next deploy.can(action, subject?)decides which controls an editor is offered. See Permissions below.appSectionsadds the host's own screens. See App sections.signOutHrefis where Sign out goes on the server forge.basePathis the framework's base path, so links to live pages resolve on a site served under one.staticDiris the repo-root-relative directory the build serves at the site root, ascmsImageSourcetakes it. It defaults to'static', and the Media section needs it to show a git forge's files at their served URLs.themeis'system'(the default),'light'or'dark'.sessionProvider,mapPathToSource,mapSourceToPathandeditorStylesheetsare for unusual hosts: a custom sign-in, a path mapping other than the default (it must match the one baked into the Editor variants), and stylesheets for the Fallback editor.
Routes
The Dashboard routes itself with hash URLs, so it needs no rewrites on a prerendered static host or in Astro:
#/pagesis the Pages section. Its search, filter and page number ride in the query, as in#/pages?q=intro&page=2, so a filtered list can be bookmarked.#/edit/<path>is the Fallback editor.#/mediais the Media section.#/globalslists the Globals, and#/globals/<name>edits one.#/app/<id>shows a slotted App section.
Signing in and out
Sign-in adapts to the forge, and the frame always shows who is signed in:
github uses the same popup session as an Editor variant. Sign out clears the token held for the repository.
local needs no sign-in, so there is no Sign out.
server uses the host's own cookie session. Sign out is a link to
signOutHref.
When sign-in fails or a session expires, the Dashboard shows a signed-out screen with a Sign in button. On github that button matters on a first visit too: a browser blocks a sign-in popup that no click opened.
Permissions
Before the Dashboard offers a control, it asks the host's can(action, subject?). Without can, everything is allowed. That is right for a static site, where repository access is the real check. The actions are those of the server forge: read-draft, save-draft, create, publish, unpublish, delete, move, restore, media-upload and media-delete, plus global-edit. The subject is the page path when an editor creates or deletes a page, the media item for media-delete, and the Global's name for global-edit. A check made before there is a subject, such as whether to show the create form at all, passes none. A host whose editors may change pages and media, but whose Globals only admins may change, passes:
import type { DashboardAction } from 'uncial-cms';
const can = (action: DashboardAction) => action !== 'global-edit' || isAdmin;can only hides controls, or shows a Global read-only. Enforcement stays with the forge: repository access on github, and the handlers' authorize on the server. Roles stay in the host, so Uncial never learns them.
Theming
The Dashboard draws with Uncial's design tokens, the same --uncial-* custom properties the editor uses. Custom properties inherit into a shadow root, so set them on the element:
uncial-dashboard {
--uncial-color-primary: light-dark(#7c2d12, #fdba74);
--uncial-font-body: 'Inter', system-ui, sans-serif;
--uncial-radius-lg: 0;
}Dark mode follows the reader's prefers-color-scheme, and the theme option fixes it to 'light' or 'dark'. A token set with light-dark() follows whichever scheme is in force. Below about 640px the nav folds into a Menu button and the page list becomes cards.
From mountIndexPage
mountIndexPage still works. It is now a deprecated wrapper that renders the Dashboard showing only Pages, with its old options. To gain Media, Globals and App sections, replace the mount with the element. Note that the element's config takes the whole site object, where mountIndexPage took site.config.