Complete guide for integrating @create-markdown packages into your project.
- Package Overview
- Installation
- Framework Integrations
- Common Patterns
- TypeScript Configuration
- Bundle Size Optimization
- Troubleshooting
The @create-markdown monorepo provides several packages:
| Package | Description |
|---|---|
@create-markdown/core |
Block-based markdown parsing and serialization (zero dependencies) |
@create-markdown/react |
React components and hooks for rendering and editing |
@create-markdown/preview |
Framework-agnostic HTML rendering with syntax highlighting (Shiki) and diagrams (Mermaid) |
@create-markdown/mdx |
Convert markdown blocks to MDX with component mappings |
create-markdown |
Convenience bundle that re-exports all packages |
Install only what you need for smaller bundle sizes:
# Core only (parsing/serialization)
pnpm add @create-markdown/core
# or
npm install @create-markdown/core
# With React support
pnpm add @create-markdown/core @create-markdown/react
# or
npm install @create-markdown/core @create-markdown/react
# With HTML preview (for non-React projects or SSG)
pnpm add @create-markdown/core @create-markdown/preview
# or
npm install @create-markdown/core @create-markdown/preview
# With syntax highlighting and diagrams
pnpm add @create-markdown/core @create-markdown/preview shiki mermaidFor projects that need everything:
pnpm add create-markdown
# or
npm install create-markdown
# With optional peer dependencies
pnpm add create-markdown react shiki mermaidpnpm add @create-markdown/core @create-markdown/reactFor interactive editors, use the 'use client' directive:
// app/components/markdown-editor.tsx
'use client';
import { useDocument, useBlockEditor, BlockRenderer } from '@create-markdown/react';
import { h1, paragraph, bulletList } from '@create-markdown/react';
export function MarkdownEditor() {
const doc = useDocument([
h1('Welcome'),
paragraph('Start editing...'),
]);
const editor = useBlockEditor(doc);
return (
<div>
<BlockRenderer
blocks={doc.blocks}
onBlockClick={(block) => editor.selectBlock(block.id)}
/>
<button onClick={() => doc.appendBlock(paragraph('New paragraph'))}>
Add Paragraph
</button>
<button onClick={() => console.log(doc.toMarkdown())}>
Export
</button>
</div>
);
}For static markdown rendering:
// app/blog/[slug]/page.tsx
import { parse } from '@create-markdown/core';
import { BlockRenderer } from '@create-markdown/react';
import fs from 'fs/promises';
async function getPost(slug: string) {
const content = await fs.readFile(`./content/${slug}.md`, 'utf-8');
return parse(content);
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const blocks = await getPost(params.slug);
return (
<article>
<BlockRenderer blocks={blocks} />
</article>
);
}Process markdown in API routes:
// app/api/markdown/route.ts
import { parse, stringify } from '@create-markdown/core';
export async function POST(request: Request) {
const { markdown } = await request.json();
const blocks = parse(markdown);
// Process blocks...
return Response.json({ blocks });
}// pages/editor.tsx
import dynamic from 'next/dynamic';
// Dynamic import for client-only editor
const MarkdownEditor = dynamic(
() => import('../components/markdown-editor'),
{ ssr: false }
);
export default function EditorPage() {
return <MarkdownEditor />;
}// components/markdown-editor.tsx
import { useDocument, BlockRenderer } from '@create-markdown/react';
import { h1, paragraph } from '@create-markdown/react';
export default function MarkdownEditor() {
const doc = useDocument([h1('Hello'), paragraph('World')]);
return <BlockRenderer blocks={doc.blocks} />;
}For SSG/SSR markdown rendering:
// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';
import { parse } from '@create-markdown/core';
import { BlockRenderer } from '@create-markdown/react';
import type { Block } from '@create-markdown/core';
import fs from 'fs/promises';
interface Props {
blocks: Block[];
}
export default function BlogPost({ blocks }: Props) {
return <BlockRenderer blocks={blocks} />;
}
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const content = await fs.readFile(`./content/${params?.slug}.md`, 'utf-8');
const blocks = parse(content);
return { props: { blocks } };
};
export const getStaticPaths: GetStaticPaths = async () => {
// Return paths for your blog posts
return { paths: [], fallback: 'blocking' };
};// src/App.tsx
import { useDocument, BlockRenderer } from '@create-markdown/react';
import { h1, paragraph, bulletList } from '@create-markdown/react';
function App() {
const doc = useDocument([
h1('My Document'),
paragraph('Edit me!'),
bulletList(['Item 1', 'Item 2', 'Item 3']),
]);
return (
<div className="app">
<BlockRenderer blocks={doc.blocks} />
<button onClick={() => {
const md = doc.toMarkdown();
navigator.clipboard.writeText(md);
}}>
Copy as Markdown
</button>
</div>
);
}
export default App;No special configuration needed. The packages export ESM and CJS:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});// app/routes/editor.tsx
import { useDocument, BlockRenderer } from '@create-markdown/react';
import { h1, paragraph } from '@create-markdown/react';
export default function Editor() {
const doc = useDocument([h1('Remix Editor'), paragraph('Start writing...')]);
return (
<div>
<BlockRenderer blocks={doc.blocks} />
</div>
);
}For loader-based markdown:
// app/routes/blog.$slug.tsx
import { json, LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { parse } from '@create-markdown/core';
import { BlockRenderer } from '@create-markdown/react';
import type { Block } from '@create-markdown/core';
import fs from 'fs/promises';
export async function loader({ params }: LoaderFunctionArgs) {
const content = await fs.readFile(`./content/${params.slug}.md`, 'utf-8');
const blocks = parse(content);
return json({ blocks });
}
export default function BlogPost() {
const { blocks } = useLoaderData<{ blocks: Block[] }>();
return <BlockRenderer blocks={blocks} />;
}---
// src/pages/blog/[slug].astro
import { parse } from '@create-markdown/core';
import BlockRenderer from '../components/BlockRenderer.tsx';
const { slug } = Astro.params;
const content = await Astro.glob(`../content/${slug}.md`);
const blocks = parse(content.rawContent());
---
<article>
<BlockRenderer blocks={blocks} client:load />
</article>// src/components/BlockRenderer.tsx
import { BlockRenderer as CMBlockRenderer } from '@create-markdown/react';
import type { Block } from '@create-markdown/core';
interface Props {
blocks: Block[];
}
export default function BlockRenderer({ blocks }: Props) {
return <CMBlockRenderer blocks={blocks} />;
}// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
});For CLI tools, scripts, or server processing:
// scripts/process-markdown.ts
import { parse, stringify, h1, paragraph } from '@create-markdown/core';
import fs from 'fs/promises';
async function processMarkdown(inputPath: string, outputPath: string) {
const content = await fs.readFile(inputPath, 'utf-8');
const blocks = parse(content);
// Add a title if missing
if (blocks[0]?.type !== 'heading') {
blocks.unshift(h1('Untitled Document'));
}
// Serialize back to markdown
const output = stringify(blocks);
await fs.writeFile(outputPath, output);
}
processMarkdown('./input.md', './output.md');// server.ts
import express from 'express';
import { parse, stringify } from '@create-markdown/core';
const app = express();
app.use(express.json());
app.post('/api/parse', (req, res) => {
const { markdown } = req.body;
const blocks = parse(markdown);
res.json({ blocks });
});
app.post('/api/stringify', (req, res) => {
const { blocks } = req.body;
const markdown = stringify(blocks);
res.json({ markdown });
});
app.listen(3000);The preview package includes a Web Component for framework-agnostic usage:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="@create-markdown/preview/themes/github.css">
</head>
<body>
<markdown-preview id="preview"></markdown-preview>
<script type="module">
import { autoRegister, blocksToHTML } from '@create-markdown/preview';
import { parse } from '@create-markdown/core';
// Register the <markdown-preview> custom element
autoRegister();
const markdown = `# Hello World
This is a **bold** statement.
\`\`\`javascript
console.log('Hello!');
\`\`\`
`;
const blocks = parse(markdown);
const html = await blocksToHTML(blocks);
document.getElementById('preview').innerHTML = html;
</script>
</body>
</html>Complete example of a WYSIWYG markdown editor:
'use client';
import { useState, useCallback } from 'react';
import {
useDocument,
useBlockEditor,
BlockRenderer
} from '@create-markdown/react';
import {
h1, h2, paragraph, bulletList, codeBlock, blockquote
} from '@create-markdown/react';
import type { Block, TextSpan } from '@create-markdown/core';
export function MarkdownEditor() {
const doc = useDocument([
h1('My Document'),
paragraph('Click to edit...'),
]);
const editor = useBlockEditor(doc);
const [preview, setPreview] = useState(false);
const handleAddBlock = useCallback((type: string) => {
switch (type) {
case 'h1': doc.appendBlock(h1('')); break;
case 'h2': doc.appendBlock(h2('')); break;
case 'paragraph': doc.appendBlock(paragraph('')); break;
case 'bullet': doc.appendBlock(bulletList([''])); break;
case 'code': doc.appendBlock(codeBlock('')); break;
case 'quote': doc.appendBlock(blockquote('')); break;
}
}, [doc]);
const handleExport = useCallback(() => {
const markdown = doc.toMarkdown();
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.md';
a.click();
URL.revokeObjectURL(url);
}, [doc]);
return (
<div className="editor">
{/* Toolbar */}
<div className="toolbar">
<button onClick={() => handleAddBlock('h1')}>H1</button>
<button onClick={() => handleAddBlock('h2')}>H2</button>
<button onClick={() => handleAddBlock('paragraph')}>¶</button>
<button onClick={() => handleAddBlock('bullet')}>•</button>
<button onClick={() => handleAddBlock('code')}>{'</>'}</button>
<button onClick={() => handleAddBlock('quote')}>"</button>
<span className="separator" />
<button onClick={() => setPreview(!preview)}>
{preview ? 'Edit' : 'Preview'}
</button>
<button onClick={handleExport}>Export</button>
</div>
{/* Content */}
<div className="content">
{preview ? (
<pre>{doc.toMarkdown()}</pre>
) : (
<BlockRenderer
blocks={doc.blocks}
onBlockClick={(block) => editor.selectBlock(block.id)}
/>
)}
</div>
</div>
);
}For read-only markdown display:
import { parse } from '@create-markdown/core';
import { BlockRenderer } from '@create-markdown/react';
interface MarkdownViewerProps {
content: string;
className?: string;
}
export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
const blocks = parse(content);
return (
<article className={className}>
<BlockRenderer blocks={blocks} />
</article>
);
}Transform markdown in build pipelines:
import { parse, stringify, createDocument } from '@create-markdown/core';
import type { Block } from '@create-markdown/core';
// Extract headings for TOC
function extractHeadings(blocks: Block[]) {
return blocks
.filter(b => b.type === 'heading')
.map(b => ({
level: b.props.level,
text: b.content.map(s => s.text).join(''),
id: b.id,
}));
}
// Add IDs to headings
function addHeadingIds(blocks: Block[]): Block[] {
return blocks.map(block => {
if (block.type === 'heading') {
const text = block.content.map(s => s.text).join('');
const id = text.toLowerCase().replace(/\s+/g, '-');
return { ...block, props: { ...block.props, id } };
}
return block;
});
}
// Process markdown file
async function processFile(content: string) {
const blocks = parse(content);
const withIds = addHeadingIds(blocks);
const toc = extractHeadings(withIds);
const markdown = stringify(withIds);
return { markdown, toc, blocks: withIds };
}Override default rendering for specific block types:
import { BlockRenderer, BlockElement } from '@create-markdown/react';
import type { Block, BlockRenderers } from '@create-markdown/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
const customRenderers: BlockRenderers = {
// Custom code block with syntax highlighting
codeBlock: ({ block }) => {
const language = block.props?.language || 'text';
const code = block.content.map(s => s.text).join('');
return (
<SyntaxHighlighter language={language} style={vscDarkPlus}>
{code}
</SyntaxHighlighter>
);
},
// Custom callout styling
callout: ({ block }) => {
const type = block.props?.type || 'info';
const icons: Record<string, string> = {
info: 'ℹ️',
warning: '⚠️',
tip: '💡',
danger: '🚨'
};
return (
<div className={`callout callout-${type}`}>
<span className="callout-icon">{icons[type]}</span>
<div className="callout-content">
<BlockElement block={block} />
</div>
</div>
);
},
// Custom image with lazy loading
image: ({ block }) => (
<figure>
<img
src={block.props.url}
alt={block.props.alt || ''}
loading="lazy"
/>
{block.props.alt && <figcaption>{block.props.alt}</figcaption>}
</figure>
),
};
export function CustomRenderer({ blocks }: { blocks: Block[] }) {
return <BlockRenderer blocks={blocks} renderers={customRenderers} />;
}Use the preview package for server-side HTML rendering with syntax highlighting:
import { parse } from '@create-markdown/core';
import {
blocksToHTML,
renderAsync,
shikiPlugin,
mermaidPlugin
} from '@create-markdown/preview';
const markdown = `
# Hello World
\`\`\`typescript
const greeting = 'Hello!';
console.log(greeting);
\`\`\`
\`\`\`mermaid
graph TD
A[Start] --> B[End]
\`\`\`
`;
// Basic HTML (no syntax highlighting)
const blocks = parse(markdown);
const basicHTML = await blocksToHTML(blocks);
// With Shiki syntax highlighting
const highlightedHTML = await renderAsync(markdown, {
plugins: [shikiPlugin({ theme: 'github-dark' })],
});
// With both Shiki and Mermaid diagrams
const fullHTML = await renderAsync(markdown, {
plugins: [
shikiPlugin({ theme: 'github-dark' }),
mermaidPlugin(),
],
});import { themes, getThemePath } from '@create-markdown/preview';
// Available themes
console.log(themes); // ['github', 'github-dark', 'minimal']
// Get path to theme CSS
const themePath = getThemePath('github-dark');
// Use in your HTML: <link rel="stylesheet" href="${themePath}">Convert markdown blocks to MDX for use with Next.js, Astro, or other MDX-compatible frameworks:
import { parse } from '@create-markdown/core';
import { blocksToMDX, configureMDXConverter } from '@create-markdown/mdx';
const markdown = `
# Welcome
This is a paragraph with **bold** text.
> A blockquote
\`\`\`javascript
console.log('Hello');
\`\`\`
`;
const blocks = parse(markdown);
// Basic MDX conversion
const mdx = blocksToMDX(blocks);
// With custom component mappings
const converter = configureMDXConverter({
components: {
codeBlock: 'CodeBlock',
blockquote: 'Callout',
},
imports: [
"import { CodeBlock } from './components/CodeBlock'",
"import { Callout } from './components/Callout'",
],
});
const customMDX = converter(blocks);{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
}
}// Core types
import type {
Block,
BlockType,
TextSpan,
InlineStyle,
Document,
DocumentMeta,
HeadingBlock,
ParagraphBlock,
CodeBlockBlock,
BulletListBlock,
CalloutBlock,
CalloutType,
MarkdownParseOptions,
MarkdownSerializeOptions,
} from '@create-markdown/core';
// React types
import type {
UseDocumentReturn,
UseMarkdownReturn,
UseBlockEditorReturn,
BlockRenderers,
BlockRendererProps,
} from '@create-markdown/react';
// Preview types
import type {
PreviewPlugin,
PreviewOptions,
ThemeName,
} from '@create-markdown/preview';// ✅ Good - tree-shakeable
import { parse, stringify } from '@create-markdown/core';
import { h1, paragraph } from '@create-markdown/core';
// ❌ Avoid - imports everything
import * as CM from '@create-markdown/core';The packages are designed for optimal tree-shaking:
// Core only (no React dependency) - ~8KB min+gzip
import { parse, stringify } from '@create-markdown/core';
// With React components - adds ~3KB
import { BlockRenderer, useDocument } from '@create-markdown/react';
// Preview package (optional deps) - ~2KB base
import { blocksToHTML } from '@create-markdown/preview';The create-markdown bundle provides subpath exports:
// Core functionality
import { parse, stringify } from 'create-markdown';
// React components (separate entry point)
import { BlockRenderer, useDocument } from 'create-markdown/react';
// Preview functionality (separate entry point)
import { blocksToHTML, shikiPlugin } from 'create-markdown/preview';Ensure your bundler supports the exports field in package.json. For older setups:
// Check your bundler's moduleResolution settings
// tsconfig.json should have: "moduleResolution": "bundler" or "node16"The library generates unique IDs for blocks. For SSR, pass stable IDs:
import { h1, paragraph } from '@create-markdown/core';
const blocks = [
{ ...h1('Title'), id: 'title-1' },
{ ...paragraph('Content'), id: 'content-1' },
];Or use the generateId function consistently:
import { generateId } from '@create-markdown/core';
// Use seeded IDs for SSR consistency
const titleId = `title-${generateId()}`;When building custom editors with contentEditable, ensure you:
- Use
suppressContentEditableWarningin React - Sync content on blur, not on every keystroke
- Handle paste events to strip formatting
- Preserve cursor position when updating innerHTML
<div
contentEditable
suppressContentEditableWarning
onBlur={(e) => handleUpdate(e.currentTarget.innerHTML)}
/>Tip: See the playground source (playground/app/components/editable-block.tsx) for a full implementation with live inline markdown rendering, cursor preservation, and list item management.
Each block needs a unique ID. The library generates UUIDs by default, but if you're creating blocks programmatically in bulk:
import { generateId, paragraph } from '@create-markdown/core';
const blocks = items.map((item, index) => ({
...paragraph(item.text),
id: `block-${index}-${generateId()}`,
}));Some packages have optional peer dependencies:
# If using syntax highlighting with @create-markdown/preview
pnpm add shiki
# If using Mermaid diagrams
pnpm add mermaid
# If using @create-markdown/react
pnpm add react react-domIf you're using path aliases, ensure they're configured:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}