Skip to main content
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.

Supported Formats

ProseKit supports multiple content formats:
FormatDescriptionBest For
JSONNative ProseMirror document structureLong-term storage, preserving all features
HTMLStandard HTML markupInteroperability with other systems, rendering content on the server side
MarkdownVia conversion librariesMarkdown-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.
1

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))
2

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
})
3

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

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)
2

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

Install dependencies

Install remark and rehype libraries:
npm install unified remark-parse remark-rehype rehype-stringify
npm install rehype-parse rehype-remark remark-stringify
2

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)
}
3

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)
}
4

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:
  1. Verify the JSON structure matches ProseKit’s schema
  2. Check that all required node types are defined in your extension
  3. Ensure HTML is well-formed and valid
  4. Check browser console for errors

Data loss on page refresh

If you’re losing data on page refresh:
  1. Ensure useDocChange is properly set up
  2. Verify localStorage is available and not full
  3. Check that the save operation completes before page unload
  4. Consider using the beforeunload event for final saves

Large documents causing performance issues

For large documents:
  1. Implement debouncing (shown above)
  2. Consider incremental saves (only save changed sections)
  3. Use compression for stored content
  4. Consider server-side storage instead of localStorage

Next Steps