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