Saving and loading content is a fundamental requirement for any editor. ProseKit provides flexible options for persisting your content in different formats, primarily JSON and HTML. This guide will show you how to implement content persistence in your ProseKit editor.
ProseKit supports multiple content formats:
| Format | Description | Best For |
|---|
| JSON | Native ProseMirror document structure | Long-term storage, preserving all features |
| HTML | Standard HTML markup | Interoperability with other systems, rendering content on the server side |
| Markdown | Via conversion libraries | Markdown-based workflows |
Working with JSON
JSON is the recommended format for storing ProseKit documents because it preserves the exact structure and all attributes of your content.
Save content as JSON
To get your document as JSON, use the getDocJSON() method:import { defineBasicExtension } from 'prosekit/basic'
import { createEditor } from 'prosekit/core'
const extension = defineBasicExtension()
const editor = createEditor({ extension })
// Get the current document as a JSON object
const json = editor.getDocJSON()
// Store the JSON
localStorage.setItem('my-document', JSON.stringify(json))
Load content from JSON
To load content from JSON when creating an editor:import { defineBasicExtension } from 'prosekit/basic'
import { createEditor, type NodeJSON } from 'prosekit/core'
const extension = defineBasicExtension()
// Retrieve stored JSON
const storedJson = localStorage.getItem('my-document')
const json: NodeJSON = storedJson ? JSON.parse(storedJson) : { type: 'doc', content: [] }
// Create editor with the loaded content
const editor = createEditor({
extension,
defaultContent: json, // Pass the JSON object directly
})
Detect document changes
To save content when the document changes, use the useDocChange hook:React:import { useDocChange } from 'prosekit/react'
useDocChange(() => {
// This runs whenever the document changes
const json = editor.getDocJSON()
localStorage.setItem('my-document', JSON.stringify(json))
}, { editor })
Vue:import { useDocChange } from 'prosekit/vue'
useDocChange(() => {
const json = editor.getDocJSON()
localStorage.setItem('my-document', JSON.stringify(json))
}, { editor })
Svelte:import { useDocChange } from 'prosekit/svelte'
useDocChange(() => {
const json = editor.getDocJSON()
localStorage.setItem('my-document', JSON.stringify(json))
}, { editor })
For performance reasons, avoid saving after every single change if your document is large. Consider debouncing the save operation to reduce the number of writes.
Debounced Saving
Here’s an example of debounced saving to improve performance:
import { useDocChange } from 'prosekit/react'
import { useCallback, useRef } from 'react'
function useAutoSave(editor, delay = 1000) {
const timeoutRef = useRef<NodeJS.Timeout>()
const save = useCallback(() => {
const json = editor.getDocJSON()
localStorage.setItem('my-document', JSON.stringify(json))
console.log('Document saved')
}, [editor])
useDocChange(() => {
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Set new timeout
timeoutRef.current = setTimeout(save, delay)
}, { editor })
}
Working with HTML
HTML is useful when you need to display your content outside the editor or integrate with systems that understand HTML.
Save content as HTML
To convert your document to HTML:import { defineBasicExtension } from 'prosekit/basic'
import { createEditor } from 'prosekit/core'
const extension = defineBasicExtension()
const editor = createEditor({ extension })
// Get the current document as an HTML string
const html = editor.getDocHTML()
// Store the HTML
localStorage.setItem('my-document-html', html)
Load content from HTML
To load content from HTML:import { defineBasicExtension } from 'prosekit/basic'
import { createEditor } from 'prosekit/core'
const extension = defineBasicExtension()
// Retrieve stored HTML
const html = localStorage.getItem('my-document-html') || ''
// Create editor with the loaded HTML
const editor = createEditor({
extension,
defaultContent: html, // Pass the HTML string
})
ProseKit automatically detects if the content is HTML or JSON based on the type. If you pass a string, it’s treated as HTML. If you pass an object, it’s treated as JSON.
Working with Markdown
While ProseKit doesn’t directly support Markdown as a storage format, you can add Markdown support using additional libraries.
Install dependencies
Install remark and rehype libraries:npm install unified remark-parse remark-rehype rehype-stringify
npm install rehype-parse rehype-remark remark-stringify
Convert Markdown to HTML
Create a utility to convert Markdown to HTML:import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
export async function markdownToHtml(markdown: string): Promise<string> {
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process(markdown)
return String(file)
}
Convert HTML to Markdown
Create a utility to convert HTML to Markdown:import { unified } from 'unified'
import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkStringify from 'remark-stringify'
export async function htmlToMarkdown(html: string): Promise<string> {
const file = await unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkStringify)
.process(html)
return String(file)
}
Use in your editor
// Save as Markdown
const html = editor.getDocHTML()
const markdown = await htmlToMarkdown(html)
localStorage.setItem('my-document-md', markdown)
// Load from Markdown
const markdown = localStorage.getItem('my-document-md') || ''
const html = await markdownToHtml(markdown)
editor.setContent(html)
Conversion Utilities
ProseKit provides utility functions for converting between plain JSON object, HTML string, and ProseMirrorNode:
htmlFromJSON - Convert JSON to HTML
htmlFromNode - Convert ProseMirror node to HTML
jsonFromHTML - Convert HTML to JSON
jsonFromNode - Convert ProseMirror node to JSON
nodeFromHTML - Convert HTML to ProseMirror node
nodeFromJSON - Convert JSON to ProseMirror node
Example Usage
import {
htmlFromJSON,
jsonFromHTML,
nodeFromJSON
} from 'prosekit/core'
// Convert JSON to HTML without an editor instance
const json = { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] }
const html = htmlFromJSON(json, editor.schema)
// Convert HTML to JSON without an editor instance
const json2 = jsonFromHTML('<p>Hello</p>', editor.schema)
// Create a ProseMirror node from JSON
const node = nodeFromJSON(json, editor.schema)
Server-Side Saving
For production applications, you’ll want to save content to a server:
import { useDocChange } from 'prosekit/react'
import { useCallback, useRef } from 'react'
function useServerAutoSave(editor, documentId: string) {
const timeoutRef = useRef<NodeJS.Timeout>()
const savingRef = useRef(false)
const save = useCallback(async () => {
if (savingRef.current) return
savingRef.current = true
try {
const json = editor.getDocJSON()
const response = await fetch(`/api/documents/${documentId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: json }),
})
if (!response.ok) {
throw new Error('Failed to save document')
}
console.log('Document saved to server')
} catch (error) {
console.error('Error saving document:', error)
} finally {
savingRef.current = false
}
}, [editor, documentId])
useDocChange(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(save, 2000)
}, { editor })
return { save }
}
Troubleshooting
Content not loading
If your content isn’t loading:
- Verify the JSON structure matches ProseKit’s schema
- Check that all required node types are defined in your extension
- Ensure HTML is well-formed and valid
- Check browser console for errors
Data loss on page refresh
If you’re losing data on page refresh:
- Ensure
useDocChange is properly set up
- Verify localStorage is available and not full
- Check that the save operation completes before page unload
- Consider using the
beforeunload event for final saves
For large documents:
- Implement debouncing (shown above)
- Consider incremental saves (only save changed sections)
- Use compression for stored content
- Consider server-side storage instead of localStorage
Next Steps