Rendering & content

Edit document-level metadata

Add typed metaFields to your schema for frontmatter-style data such as title, author, publish date, or tags. Metadata is stored as JSON on the top-level document meta field and is not serialized to YAML.

<script lang="ts">
	import {
		DocumentMetaPanel,
		Editor,
		createDocumentMetaController,
		createSchema
	} from 'uncial';

	const schema = createSchema(blocks, {
		metaFields: {
			title: { default: '', required: true },
			author: { default: '' },
			publishedAt: { default: '', placeholder: 'YYYY-MM-DD' },
			tags: { default: [], input: 'json' }
		}
	});

	const metaController = createDocumentMetaController(schema.metaFields);
	let document = $state({ type: 'doc', content: [{ type: 'paragraph' }] });
	let meta = $state({ title: 'Hello world', author: 'Ada' });
</script>

<Editor {blocks} {schema} {metaController} bind:json={document} bind:meta />
<DocumentMetaPanel
	controller={metaController}
	fields={schema.metaFields}
	onCommit={(nextMeta) => (meta = nextMeta)}
/>

When metadata fields are configured, Editor also shows a built-in Metadata button in the toolbar. The renderer normalizes the same metadata and exposes it as an optional snippet prop.

<Renderer content={document} {blocks} {schema}>
	{#snippet meta(meta)}
		<h1>{meta?.title}</h1>
	{/snippet}
</Renderer>

Match Uncial to your site

Uncial's editor and renderer chrome are plain CSS. Tailwind and DaisyUI are not required by the library. Import the default styles once, then override --uncial-* custom properties anywhere above the editor in the cascade.

// Full default editor, renderer, controls, and rich text prose styles.
import 'uncial/styles';

// Or import only the editor chrome if your app owns prose typography.
import 'uncial/styles/chrome';

Scope theme tokens to a wrapper when one page should look different from the rest of your app:

.your-custom-class .uncial-editor-shell,
.your-custom-class .uncial-renderer {
	--uncial-color-surface: white;
	--uncial-color-primary: #7c3aed;
	--uncial-color-primary-contrast: white;
	--uncial-color-accent: #d97706;

	--uncial-radius-md: 0.5rem;
	--uncial-font-body: Inter, system-ui, sans-serif;
	--uncial-font-display: Georgia, serif;
	--uncial-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
}

The most common tokens are:

  • --uncial-color-primary — Primary buttons, links, active controls, block focus outlines.

  • --uncial-color-primary-contrast — Text/icons on primary buttons.

  • --uncial-color-accent — Secondary emphasis color for consumers who want one.

  • --uncial-color-surface — Main editor and renderer background.

  • --uncial-color-surface-elevated — Toolbar, inputs, nested block surfaces.

  • --uncial-color-border — Control, panel, renderer, and block chrome borders.

  • --uncial-color-text — Editor UI and rich text foreground.

  • --uncial-color-code-bg — Fenced code block background. Defaults dark in both light and dark themes.

  • --uncial-radius-sm/md/lg — Buttons, inputs, panels, renderer, and block chrome radius.

  • --uncial-font-body/display/mono — Editor UI, headings, and code typography.

Rich text uses a stable .uncial-rich-content hook. If your site already has article typography, import uncial/styles/chrome and style that hook yourself:

.your-custom-class .uncial-rich-content {
	font: 400 1rem/1.7 var(--site-body-font);
	color: var(--site-ink);
}

.your-custom-class .uncial-rich-content h2 {
	font: 700 1.75rem/1.2 var(--site-display-font);
	margin-block: 2rem 0.75rem;
}

Normalize and validate before publish

Use normalizeDocument and validateDocument around persistence boundaries, or pass onIssue to Editor and Renderer to observe document issues as they happen.

// src/lib/publish.ts
import { normalizeDocument, validateDocument } from 'uncial';
import { blocks, schema } from './uncial';

const normalized = normalizeDocument(document, blocks, schema);
const result = validateDocument(normalized, blocks, schema, {
	onIssue: (issue) => console.warn(issue.code, issue.path)
});

if (!result.ok) {
	throw new Error('Document is not publishable');
}

Render on the server without the editor

"Same component in editor and frontend" only works if the frontend half stays lean. The Renderer is safe to import in a server bundle. Use the granular entry points so the browser-only editor stack (Tiptap, node views, mount()) never reaches your server.

// Server-safe: no editor mount code enters your bundle.
import { Renderer } from 'uncial/render';
import { createBlockRegistry, createSchema, defineSvelteBlock } from 'uncial/core';

uncial/render and uncial/core are free of editor machinery, and a regression test walks the render/ import graph to keep it that way. The package root (import ... from 'uncial') also re-exports the editor, so prefer these entry points on the server.

Image attributes need one extra step when your site is served under a base path, such as a GitHub Pages project site. Stored image URLs start at the site root (/media/photo.webp), so add the base when you render them with resolveImageSrc:

<!-- src/lib/blocks/Figure.svelte -->
<script lang="ts">
	import { base } from '$app/paths';
	import { resolveImageSrc } from 'uncial/render';

	let { src = '', alt = '' } = $props();
</script>

{#if src}<img src={resolveImageSrc(src, base)} {alt} />{/if}

resolveImageSrc only changes URLs that start with a single /. Full URLs, data: URLs and the editor's blob: previews pass through unchanged, so the same component works in the editor and on the live site.