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