Markdown Examples

Beta

Real-world examples and recipes for common use cases with the Markdown extension.

Basic Examples

Read and Write Markdown

This example demonstrates the most common Markdown operations:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'

const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [StarterKit, Markdown],
  content: '# Hello World\n\nStart typing...',
  contentType: 'markdown', // parse initial content as Markdown
})

// Read: serialize current editor content to Markdown
console.log(editor.getMarkdown())

// Write: set editor content from a Markdown string
editor.commands.setContent('# New title\n\nSome *Markdown* content', { contentType: 'markdown' })

Paste Markdown Detection

Automatically detect and parse pasted Markdown:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { Plugin } from '@tiptap/pm/state'

const PasteMarkdown = Extension.create({
  name: 'pasteMarkdown',

  addProseMirrorPlugins() {
    return [
      new Plugin({
        props: {
          handlePaste(view, event, slice) {
            const text = event.clipboardData?.getData('text/plain')

            if (!text) {
              return false
            }

            // Check if text looks like Markdown
            if (looksLikeMarkdown(text)) {
              const { state, dispatch } = view
              // Parse the Markdown text to Tiptap JSON using the Markdown manager
              const json = editor.markdown.parse(text)

              // Insert the parsed JSON content at cursor position
              editor.commands.insertContent(json)
              return true
            }

            return false
          },
        },
      }),
    ]
  },
})

function looksLikeMarkdown(text: string): boolean {
  // Simple heuristic: check for Markdown syntax
  return (
    /^#{1,6}\s/.test(text) || // Headings
    /\*\*[^*]+\*\*/.test(text) || // Bold
    /\[.+\]\(.+\)/.test(text) || // Links
    /^[-*+]\s/.test(text)
  ) // Lists
}

const editor = new Editor({
  extensions: [StarterKit, Markdown, PasteMarkdown],
})

Custom Tokenizers

Subscript and Superscript

Support ~subscript~ and ^superscript^:

import { Mark } from '@tiptap/core'

export const Subscript = Mark.create({
  name: 'subscript',

  parseHTML() {
    return [{ tag: 'sub' }]
  },

  renderHTML() {
    return ['sub', 0]
  },

  markdownTokenName: 'subscript',

  parseMarkdown: (token, helpers) => {
    const content = helpers.parseInline(token.tokens || [])
    return helpers.applyMark('subscript', content)
  },

  renderMarkdown: (node, helpers) => {
    const content = helpers.renderChildren(node.content || [])
    return `~${content}~`
  },

  markdownTokenizer: {
    name: 'subscript',
    level: 'inline',
    start: (src) => src.indexOf('~'),
    tokenize: (src, tokens, lexer) => {
      const match = /^~([^~]+)~/.exec(src)
      if (!match) return undefined

      return {
        type: 'subscript',
        raw: match[0], // Full match: ~text~
        text: match[1], // Content: text
        tokens: lexer.inlineTokens(match[1]), // Parse nested inline formatting
      }
    },
  },
})

export const Superscript = Mark.create({
  name: 'superscript',

  parseHTML() {
    return [{ tag: 'sup' }]
  },

  renderHTML() {
    return ['sup', 0]
  },

  markdownTokenName: 'superscript',

  parseMarkdown: (token, helpers) => {
    const content = helpers.parseInline(token.tokens || [])
    return helpers.applyMark('superscript', content)
  },

  renderMarkdown: (node, helpers) => {
    const content = helpers.renderChildren(node.content || [])
    return `^${content}^`
  },

  markdownTokenizer: {
    name: 'superscript',
    level: 'inline',
    start: (src) => src.indexOf('^'),
    tokenize: (src, tokens, lexer) => {
      const match = /^\^([^^]+)\^/.exec(src)
      if (!match) return undefined

      return {
        type: 'superscript',
        raw: match[0], // Full match: ^text^
        text: match[1], // Content: text
        tokens: lexer.inlineTokens(match[1]), // Parse nested inline formatting
      }
    },
  },
})

Usage:

editor.commands.setContent('H~2~O and E = mc^2^', { contentType: 'markdown' })

Integration Examples

Real-Time Markdown Preview

You can create a real-time Markdown preview by listening to editor updates:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'

const editor = new Editor({
  extensions: [StarterKit, Markdown],
  content: '# Hello',
  onUpdate: ({ editor }) => {
    const markdown = editor.getMarkdown()
    updatePreview(markdown) // Your preview update function
  },
})

function updatePreview(markdown) {
  document.querySelector('#preview').textContent = markdown
}

Saving and Loading Workflow

Store content as Markdown and load it when needed:

// Save to database/storage
async function saveContent() {
  const markdown = editor.getMarkdown()
  await fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify({ content: markdown }),
  })
}

// Load from database/storage
async function loadContent() {
  const { content } = await fetch('/api/load').then((r) => r.json())
  editor.commands.setContent(content, { contentType: 'markdown' })
}

Server-Side Rendering

Render Markdown on the server:

import StarterKit from '@tiptap/starter-kit'
import { MarkdownManager } from '@tiptap/markdown'
import { generateHTML } from '@tiptap/html'

const markdownManager = new MarkdownManager({
  extensions: [StarterKit, Markdown], // Include Markdown extension
})

// Parse Markdown to JSON on server
function parseMarkdown(markdown: string) {
  return editor.markdownManager.parse(markdown)
}

// Convert JSON to HTML for rendering
function renderToHTML(json: JSONContent) {
  // Generate HTML from Tiptap JSON (no Markdown involved here)
  return generateHTML(json, [StarterKit])
}

// Full pipeline: Markdown → JSON → HTML
function markdownToHTML(markdown: string) {
  const json = parseMarkdown(markdown) // Parse Markdown to JSON
  return renderToHTML(json) // Render JSON to HTML
}

// Express route example
app.get('/document/:id', async (req, res) => {
  const doc = await db.getDocument(req.params.id)
  const json = parseMarkdown(doc.markdown) // Parse stored markdown
  const html = renderToHTML(json) // Convert to HTML for display

  res.render('document', { content: html })
})

Advanced Patterns

Lazy Loading Large Documents

Load large documents progressively:

async function loadLargeDocument(documentId: string) {
  // Load metadata first
  const meta = await fetchDocumentMeta(documentId)

  // Show skeleton
  showSkeleton()

  // Load in chunks
  const chunks = await fetchDocumentChunks(documentId, meta.chunkCount)

  // Parse each Markdown chunk and insert at correct position
  for (const chunk of chunks) {
    const json = editor.markdown.parse(chunk.markdown) // Parse Markdown to JSON
    editor.commands.insertContentAt(chunk.position, json) // Insert at position
  }

  hideSkeleton()
}