Sync
Save to the file system with autosave and live merge
Sync connects the editor to a directory of .md and .mdx files:
- edits autosave to disk
- changes on disk (a
git pull, a save from your IDE) merge into the open document without moving your caret - a block changed on both sides becomes a conflict you resolve in one click
Setup
Install
npm install @fumadocs-editor/coreAdd the Vite plugin
It runs the sync server inside vite dev. root is the directory to edit.
import { defineConfig } from "vite";
import { editorSync } from "@fumadocs-editor/core/vite";
export default defineConfig({
plugins: [editorSync({ root: "content/docs" })],
});
Keep Vite away from the edited files
If root sits inside your Vite project, exclude it from the watcher, or every save reloads the page:
export default defineConfig({
server: { watch: { ignored: ["**/content/docs/**"] } },
});Never import those files as modules either (?raw included).
Point the editor at a file
import { MdxEditor } from "@fumadocs-editor/ui";
export function SyncedEditor({ path }: { path: string }) {
return <MdxEditor sync={{ path }} />;
}
The editor now loads the file, autosaves and shows a status dot in its header.
Saving
- Autosave runs 800 ms after you stop typing, and at most 5 s after the
first unsaved change.
⌘Sand closing the page save immediately. - A save never overwrites a file it has not seen. If the file changed on disk in the meantime, that change is merged in first.
- When the same block changed on both sides, your version is kept and saving pauses until you pick keep mine or take disk.
The status dot shows where the document stands. Mirror it elsewhere with
sync.onStatus.
| Status | Meaning |
|---|---|
synced | disk matches what you see |
dirty | edits waiting for autosave |
saving | a save is in flight |
conflict | the same block changed locally and on disk |
offline | disconnected; edits are kept and saved later |
denied | the server rejected your credentials |
Sync client
By default, every editor on the page shares one connection to the dev server. Create your own client to connect elsewhere, send credentials or turn on collaboration, then pass it to every editor:
import { createSyncClient, wsTransport } from "@fumadocs-editor/core/sync";
import { MdxEditor } from "@fumadocs-editor/ui";
const client = createSyncClient({
transport: wsTransport("ws://localhost:3100/sync"),
});
export function Editor({ path }: { path: string }) {
return <MdxEditor sync={{ client, path }} />;
}
Prop
Type
Standalone server
Without Vite, createSyncServer gives you handlers for any Node HTTP server:
import { createServer } from "node:http";
import { createSyncServer } from "@fumadocs-editor/core/node";
const sync = createSyncServer({ root: "content/docs" });
const server = createServer((request, response) => {
const url = request.url ?? "/";
if (url.startsWith("/upload")) return sync.handleUpload(request, response);
if (url.startsWith("/assets/")) {
// handleAsset reads the path after the mount prefix, like a middleware
request.url = url.slice("/assets".length);
return sync.handleAsset(request, response);
}
response.statusCode = 404;
response.end();
});
server.on("upgrade", (request, socket, head) => {
if (request.url === "/sync") sync.handleUpgrade(request, socket, head);
});
server.listen(3100);
The Vite plugin accepts the same options:
Prop
Type
Other backends
A database, a CMS or a worker can replace the Node server by speaking the sync protocol.
Without MdxEditor
sync is built on client.open. Use it directly for a custom status UI or a
non-React shell:
import { useEffect, useRef, useState } from "react";
import { MdxEditor, type MdxEditorRef } from "@fumadocs-editor/ui";
import type { DocumentStatus, SyncClient, SyncDocument } from "@fumadocs-editor/core/sync";
export function CustomDocument({ client, path }: { client: SyncClient; path: string }) {
const editorRef = useRef<MdxEditorRef>(null);
const documentRef = useRef<SyncDocument>(null);
const [status, setStatus] = useState<DocumentStatus>("synced");
useEffect(() => {
const editor = editorRef.current!;
const doc = client.open(path, { editor });
documentRef.current = doc;
doc.opened.then(({ text }) => editor.setMarkdown(text), console.error);
const stop = doc.subscribe(() => setStatus(doc.status()));
return () => {
stop();
doc.close();
};
}, [client, path]);
return (
<>
<p>{status}</p>
<MdxEditor ref={editorRef} onChange={() => documentRef.current?.changed()} />
</>
);
}
editoris anything withgetMarkdown,applyExternalMarkdown,setMarkdownandmarkSaved. The editor ref qualifies.- Load
opened'stextinto the editor, callchanged()on every edit. - A client holds one document per path.
Prop
Type
Last updated on