Skip to content

Latest commit

 

History

History
999 lines (767 loc) · 22.7 KB

File metadata and controls

999 lines (767 loc) · 22.7 KB

Integration Guide

Complete guide for integrating @create-markdown packages into your project.

Table of Contents


Package Overview

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

Installation

Individual Packages (Recommended)

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 mermaid

Convenience Bundle

For projects that need everything:

pnpm add create-markdown
# or
npm install create-markdown

# With optional peer dependencies
pnpm add create-markdown react shiki mermaid

Framework Integrations

Next.js (App Router)

Installation

pnpm add @create-markdown/core @create-markdown/react

Client Components

For 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>
  );
}

Server Components

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

API Routes

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

Next.js (Pages Router)

// 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' };
};

Vite + React

// 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;

Vite Configuration

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()],
});

Remix

// 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} />;
}

Astro

Static Markdown Pages

---
// 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>

React Component in Astro

// 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()],
});

Node.js / Server-side

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');

Express.js API

// 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);

Vanilla JavaScript / Web Components

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>

Common Patterns

Building a Markdown Editor

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

Rendering Markdown Content

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

Server-side Markdown Processing

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 };
}

Custom Block Renderers

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} />;
}

HTML Preview with Syntax Highlighting

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(),
  ],
});

Using Preview Themes

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}">

MDX Conversion

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

TypeScript Configuration

Recommended tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  }
}

Type Imports

// 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';

Bundle Size Optimization

Import Only What You Need

// ✅ 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';

Use Separate Packages

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';

Convenience Bundle Import Paths

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';

Troubleshooting

"Module not found: @create-markdown/react"

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"

SSR Hydration Mismatch

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

contentEditable Issues

When building custom editors with contentEditable, ensure you:

  1. Use suppressContentEditableWarning in React
  2. Sync content on blur, not on every keystroke
  3. Handle paste events to strip formatting
  4. 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.

Block ID Collisions

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()}`,
}));

Peer Dependency Warnings

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-dom

TypeScript Path Resolution

If you're using path aliases, ensure they're configured:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Need Help?