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:

  • config is required: the site object from defineSite or defineServerSite, or a bare site config.

  • blocks and schema are required, as the route factories take them.

  • globals names the site's Globals and their schemas. It defaults to the site object's globals.

  • 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.

  • appSections adds the host's own screens. See App sections.

  • signOutHref is where Sign out goes on the server forge.

  • basePath is the framework's base path, so links to live pages resolve on a site served under one.

  • staticDir is the repo-root-relative directory the build serves at the site root, as cmsImageSource takes it. It defaults to 'static', and the Media section needs it to show a git forge's files at their served URLs.

  • theme is 'system' (the default), 'light' or 'dark'.

  • sessionProvider, mapPathToSource, mapSourceToPath and editorStylesheets are 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:

  • #/pages is 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.

  • #/media is the Media section.

  • #/globals lists 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.