Quick start: Nuxt
Install the peers next to the package (vue, @nuxt/ui, @tiptap/core, @tiptap/pm, @tiptap/vue-3, yjs, @hocuspocus/provider, tailwindcss, @iconify-json/lucide; see Installation), then follow the steps. Inside the repository declare "@skaile/forge-editor": "workspace:*" instead of the npm version.
Register the module after @nuxt/ui
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false, // TipTap renders on the client only
modules: ['@nuxt/ui', '@skaile/forge-editor/nuxt'],
css: ['~/assets/main.css'],
})
The module patches Nuxt UI's generated ui.css template and throws if that template is missing, so @nuxt/ui must come first. It adds every feature stylesheet to css (themes.css last), puts the package into build.transpile and Nitro's externals.inline, enables Nitro websockets, installs the client plugin editor-context-nuxt-ui.client.ts (createEditor({ kit: nuxtUiKit(), backend: createHttpBackend({ fetch: $fetch, probe: true }) }); the module has no kit option, the Vue binding is Nuxt UI) and scans the Nitro routes. It registers no component or composable.
Module options live under the config key tiptapEditor:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false, // TipTap renders on the client only
modules: ['@nuxt/ui', '@skaile/forge-editor/nuxt'],
tiptapEditor: { ai: { /* optional; presets (Model presets), roles (the default Model preset per Model role), rateLimit, transcription: see AI configuration */ }, collab: { authenticateUpgrade: false /* true: refuse an unauthenticated /collab upgrade with HTTP 401 (cookie hosts only) */ } },
})
The ai options are described in AI configuration.
Add the stylesheet
/* app/assets/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
Mount the simple editor
Wrap the app in <UApp> (app/app.vue: <UApp><NuxtPage /></UApp>) and mount an editor on a page (the simple editor needs no backend):
<script setup lang="ts">
import { SimpleEditor } from '@skaile/forge-editor/vue/simple'
import { ref } from 'vue'
const content = ref()
</script>
<template>
<SimpleEditor v-model="content" />
</template>
Register EditorPorts
The module's API routes call the host's EditorPorts (identity first, then one port per capability: documents, collabState, directory, assets, settings, themes, models, comments). Hand them to the module with provideEditorServer({ ports }) in a Nitro plugin; the module installs the editor server at the first request, and without provided ports the client logs „Einstellungen konnten nicht geladen werden“ and every route answers 500. A route whose port you leave out answers 501. These minimal ports keep everything in memory and let any visitor in (replace authenticate and the storage with yours; Server ports lists every member, the routes that call it and the required results):
// server/plugins/editor-adapter.ts
import type { JSONContent } from '@tiptap/core'
import { type AssetStore } from '@skaile/forge-editor/server'
import { provideEditorServer } from '@skaile/forge-editor/server/nitro'
export default defineNitroPlugin(() => {
const contents = new Map<string, JSONContent>()
const states = new Map<string, Uint8Array>()
const assets = new Map<string, Parameters<AssetStore['storeAsset']>[1]>()
provideEditorServer({
ports: {
// `null` = 401 on API routes; on /collab the socket is closed with 4401 (HTTP 401 at the upgrade with `collab.authenticateUpgrade`)
identity: { authenticate: async () => ({ id: 'demo', username: 'demo' }) },
documents: {
loadContent: async id => contents.get(id) ?? null,
storeContent: async (id, content) => { contents.set(id, content) },
listContents: async () => [...contents].map(([id, content]) => ({ id, content })),
},
collabState: {
loadState: async id => states.get(id) ?? null,
storeState: async (id, state) => { states.set(id, state) },
removeState: async (id) => { states.delete(id) },
},
directory: { listUsers: async () => [] },
assets: {
storeAsset: async (id, asset) => { assets.set(`${id}/${asset.name}`, asset) },
loadAsset: async (id, name) => {
const asset = assets.get(`${id}/${name}`)
if (!asset) return null
const { data, ...record } = asset
return { record, data }
},
},
// Omit `settings`: settings stay in the browser (GET answers 204).
// Omit `themes`: the themes routes answer 501 and the standard look stays.
},
})
})
Mount the advanced editor
app.vue must wrap pages in <UApp> (overlays, toasts, tooltips). The toolbar is a sibling that receives the editor from ready:
<script setup lang="ts">
import type { Editor, JSONContent } from '@tiptap/vue-3'
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import { EditorToolbar } from '@skaile/forge-editor/vue'
import { ref, shallowRef } from 'vue'
const content = ref<JSONContent>()
const editor = shallowRef<Editor>()
</script>
<template>
<EditorToolbar :editor="editor" />
<AdvancedEditor v-model="content" @ready="editor = $event" />
</template>
Without a collaboration prop the editor is standalone (JSON v-model, own undo history). Collaboration is a mode fixed at mount: pass :collaboration="{ ydoc, provider, user }" and connect the provider to /collab/:id (Collaboration).
AdvancedEditor pulls in every feature. For a smaller bundle use SimpleEditor or compose your own list (Feature composition). For the simple editor in plain Vue (no Nuxt, no backend) follow the plain Vue quick start.
Next steps
The advanced editor, every adapter method, collaboration and AI are covered in the guides: Server ports, Routes and authentication, Collaboration and AI configuration. The tutorial builds an editor from the simple editor up.