mdream Documentation

repository·main·Indexed 21 days ago

https://github.com/harlan-zw/mdream

A high-performance HTML to Markdown converter optimized for LLM token efficiency and speed. mdream provides a Rust library API, a CLI tool, and various platform-specific binaries for Node.js (including Android, macOS, Linux, and WASM). It includes a Nuxt module for dynamic and static Markdown generation, and a GitHub Action (@mdream/action) to process prerendered HTML files into llms.txt artifacts for CI/CD workflows.

Tokens
67.7K
Snippets
221
Records
297
Agent score
74%

What's inside mdream

  1. What is Mdream?

    main
    Mdream is a zero-dependency, LLM-optimized HTML to Markdown converter. It is designed to be faster and more token-efficient than alternatives like Turndown or node-html-markdown, producing output specifically tuned for readability and reduced token costs in Large Language Model (LLM) workflows.
  2. Identify the `@mdream/rust-linux-arm64-musl` package

    main
    The @mdream/rust-linux-arm64-musl package provides the aarch64-unknown-linux-musl target binary for @mdream/rust. Use this package when you need to run @mdream/rust on Linux systems with ARM64 architecture using the musl C library (common in lightweight or containerized environments like Alpine Linux).
  3. Choose the correct mdream Docker image

    main

    mdream provides two distinct Docker images depending on whether you need to convert existing HTML or crawl live websites:

    1. harlanzw/mdream:core: Use this for high-performance HTML to Markdown conversion. It contains a single native Rust binary (~600 KB). It reads from stdin and writes to stdout. It cannot fetch URLs or render JavaScript.
    2. harlanzw/mdream:crawl: Use this for fetching URLs, crawling entire websites, and generating llms.txt files. It includes Node.js and Playwright Chrome (~1.5 GB) to handle JavaScript-heavy sites.

    Note: harlanzw/mdream:latest is an alias for :crawl. It is recommended to use the explicit :core or :crawl tags.

    | Image | Use it for | Engine | Size |
    |-------|-----------|--------|------|
    | `harlanzw/mdream:core` | Converting HTML you already have to Markdown | Native Rust binary | ~600 KB |
    | `harlanzw/mdream:crawl` | Fetching/crawling URLs, `llms.txt` generation | Node + Playwright Chrome | ~1.5 GB |
  4. Overview of Mdream Packages

    main

    Mdream provides several specialized packages depending on your environment and use case:

    PackageDescription
    mdreamRust NAPI engine + WASM for edge. Performance-first, declarative config. Includes CLI.
    @mdream/jsPure JS engine. Full hook access, zero native deps. Tree-shakable via /core.
    @mdream/crawlSite-wide crawler to generate llms.txt artifacts from entire websites.
    DockerPre-built images for core conversion and crawl (with Playwright Chrome).
    @mdream/viteAutomatically generate .md for Vite sites.
    @mdream/actionGenerate .md and llms.txt from static .html output.
    mdream (crate)Native Rust crate with CLI, zero dependencies, and streaming support.
    Browser CDNUse via unpkg/jsDelivr without build steps.
  5. Targeted crawling with Glob Patterns

    main

    URLs support glob patterns for targeted crawling. When a glob pattern is provided, the crawler uses sitemap discovery to find all matching URLs. Patterns are matched using picomatch syntax. A trailing single * (e.g., /fieldtypes*) expands to match both the path itself and all subdirectories.

    # Crawl only the /docs/ section
    npx @mdream/crawl -u "docs.example.com/docs/**"
    
    # Crawl pages matching a prefix
    npx @mdream/crawl -u "example.com/blog/2024*"
  6. How @mdream/vite behaves in different Vite modes

    main

    The plugin's behavior changes depending on the Vite command being run:

    Development (vite dev)

    Intercepts incoming requests via middleware. If a request matches the Request Matching criteria, it resolves the HTML path (checking .html, the base path, or /index.html), converts it to Markdown using mdream, and responds with Content-Type: text/markdown; charset=utf-8 and Cache-Control: no-cache.

    Build (vite build)

    Processes all HTML assets in the output bundle using the generateBundle hook. For files matching include and not matching exclude, it converts the HTML to Markdown and emits a corresponding .md file into the bundle (preserving directory structure). You can use outputDir to place these files in a specific subdirectory.

    Preview (vite preview)

    Uses middleware to read built HTML files from the output directory (default dist). It resolves paths in the order: <outDir>/<basePath>.html, <outDir>/<basePath>/index.html, or <outDir>/index.html. Responses use Cache-Control: public, max-age=3600.

  7. Understand the mdream rendering engines

    main

    Mdream provides two engines that can be used depending on your environment and feature requirements:

    1. Rust (NAPI): Provided by the mdream package. It is the default for Node.js and uses declarative configuration only. It is highly performant due to native bindings.
    2. Rust (WASM): Provided by the mdream package. Suitable for Edge environments or the browser.
    3. JavaScript: Provided by the @mdream/js package. This engine supports both declarative configuration and hook-based plugins (imperative transforms). It is also used for the Markdown splitter.

    Both engines support the same declarative plugin configuration keys: origin, minimal, frontmatter, isolateMain, tailwind, filter, extraction, tagOverrides, and clean.

  8. Request Matching criteria for @mdream/vite

    main

    The middleware intercepts requests when:

    1. Explicit .md extension: The URL ends in .md (e.g., /about.md).
    2. Content negotiation: The client's Accept header prefers text/markdown or text/plain over text/html.

    Note: A bare */* wildcard in the Accept header does not trigger Markdown serving. Requests with Sec-Fetch-Dest: document are always served as HTML.

    Skipped Paths:

    • /api/* (API routes)
    • /_* (internal routes)
    • /@* (Vite internal routes)
    • Any path with a file extension other than .md (e.g., .js, .css, .html, .json).

    URL Mapping Examples:

    Request PathResolved HTML
    /about.md/about.html or /about or /index.html
    /docs/guide.md/docs/guide.html or /docs/guide or /index.html
    /index.md/ (special case: /index maps to /)
  9. Static Generation and llms.txt support

    main

    When using nuxt generate or configuring nitro.prerender.routes, the module automatically performs the following:

    1. Generates .md files alongside HTML for all prerendered pages.
    2. Creates llms.txt with a page listing (using site name and description from nuxt-site-config).
    3. Creates llms-full.txt containing the full markdown content of all pages.

    These files are written to the Nitro public output directory and served as static assets.

  10. Understand the output formats

    main

    The crawler generates three types of output:

    1. Individual Markdown Files: One .md file per crawled page, organized in the output directory following the original URL path structure (e.g., https://example.com/docs/intro becomes output/docs/intro.md).
    2. llms.txt: A site overview file following the llms.txt specification, containing a list of all crawled pages with their titles and links to their respective markdown files.
    3. llms-full.txt: Similar to llms.txt, but includes the full markdown content of every page inline.
  11. Use crawl hooks to transform data

    main

    The @mdream/crawl package provides six hooks that allow you to intercept and transform data at different stages of the crawl pipeline. Hooks receive mutable objects; you must mutate them in-place to apply changes. You can use these via defineConfig or by passing them directly to the crawlAndGenerate function.

    Available hooks:

    • crawl:url: Called before fetching. Set ctx.skip = true to skip the network request.
    • crawl:html: Called after fetching, before HTML-to-Markdown conversion. Mutate ctx.html to transform raw HTML.
    • crawl:page: Called after HTML-to-Markdown conversion. Mutate page.title, page.metadata, etc. (replaces the legacy onPage callback).
    • crawl:content: Called before markdown is written to disk. Mutate ctx.content or ctx.filePath.
    • crawl:done: Called after all pages are crawled, before llms.txt generation. Use ctx.results to filter or reorder results.
    import { crawlAndGenerate, defineConfig } from '@mdream/crawl'
    
    // Example using defineConfig
    export default defineConfig({
      hooks: {
        'crawl:url': (ctx) => {
          if (ctx.url.includes('/assets/')) ctx.skip = true
        },
        'crawl:page': (page) => {
          page.title = page.title.replace(/ - Docs$/, '')
        },
        'crawl:done': (ctx) => {
          const filtered = ctx.results.filter(r => r.content.length > 100)
          ctx.results.length = 0
          ctx.results.push(...filtered)
        }
      },
    })
    
    // Example using programmatic API
    await crawlAndGenerate({
      urls: ['https://example.com'],
      outputDir: './output',
      hooks: {
        'crawl:page': (page) => {
          page.title = page.title.replace(/ \| Brand$/, '')
        }
      },
    })
  12. Create custom transform logic with Hook-Based Plugins

    main

    The JS engine (@mdream/js) allows you to intercept and modify the conversion pipeline using imperative hook-based plugins. You can create a plugin using createPlugin from @mdream/js/plugins and pass it to the hooks option in htmlToMarkdown.

    Available hooks include:

    • beforeNodeProcess: Intercept before any node processing. Return { skip: true } to skip the node.
    • onNodeEnter: Prepend a string when entering an element node.
    • onNodeExit: Append a string when exiting an element node.
    • processAttributes: Modify element attributes (e.g., for Tailwind class extraction).
    • processTextNode: Transform text nodes. Return { content: string, skip: boolean }.
    import { htmlToMarkdown } from '@mdream/js'
    import { createPlugin } from '@mdream/js/plugins'
    
    const myPlugin = createPlugin({
      onNodeEnter(node) {
        if (node.name === 'h1')
          return '** '
      },
      processTextNode(textNode) {
        if (textNode.parent?.attributes?.id === 'highlight') {
          return { content: `**${textNode.value}**`, skip: false }
        }
      },
    })
    
    const markdown = htmlToMarkdown(html, { hooks: [myPlugin] })