<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vigoss Luke</title>
    <description>The latest articles on DEV Community by Vigoss Luke (@vigoss_luke_3604c1d0e9b4a).</description>
    <link>https://dev.to/vigoss_luke_3604c1d0e9b4a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3966761%2Fc4836572-bf0b-4876-a110-78622d1c034b.png</url>
      <title>DEV Community: Vigoss Luke</title>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vigoss_luke_3604c1d0e9b4a"/>
    <language>en</language>
    <item>
      <title>npm v12 Just Shipped — Here's What Broke in My CI and How I Fixed It</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Thu, 09 Jul 2026 08:23:21 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/npm-v12-just-shipped-heres-what-broke-in-my-ci-and-how-i-fixed-it-3dgc</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/npm-v12-just-shipped-heres-what-broke-in-my-ci-and-how-i-fixed-it-3dgc</guid>
      <description>&lt;p&gt;npm v12 hit stable on July 8. Within 12 hours, three of our CI pipelines were red. Not yellow — red. And the worst part: one of them was green for 4 hours before anyone noticed the deploy was shipping broken containers.&lt;/p&gt;

&lt;p&gt;Here's exactly what broke, why it was silent, and how to fix each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One That Scared Me: &lt;code&gt;npm ci&lt;/code&gt; Exits 0, Native Modules Never Compiled
&lt;/h2&gt;

&lt;p&gt;This is the trap. Your CI runs &lt;code&gt;npm ci&lt;/code&gt;, it exits with code 0. Docker build succeeds. Container deploys. App starts. Everything looks fine — until the first request that hits a native module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: Cannot find module &lt;span class="s1"&gt;'./build/Release/sharp.node'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what happened under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
&lt;span class="c"&gt;# v12 behavior: encounters sharp's install script&lt;/span&gt;
&lt;span class="c"&gt;# → script blocked (not in allowlist)&lt;/span&gt;
&lt;span class="c"&gt;# → no error, no warning in exit code&lt;/span&gt;
&lt;span class="c"&gt;# → npm ci exits 0&lt;/span&gt;
&lt;span class="c"&gt;# → node_modules/sharp exists, but build/Release/ is empty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix isn't &lt;code&gt;--allow-scripts=all&lt;/code&gt; in CI. That defeats the purpose. The fix is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: Audit your actual script needs&lt;/span&gt;
npm approve-scripts &lt;span class="nt"&gt;--allow-scripts-pending&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: This prints the real list — usually 3-8 packages, not 200&lt;/span&gt;
&lt;span class="c"&gt;# Example output:&lt;/span&gt;
&lt;span class="c"&gt;#   sharp (postinstall: node-gyp rebuild)&lt;/span&gt;
&lt;span class="c"&gt;#   esbuild (postinstall: node install.js)&lt;/span&gt;
&lt;span class="c"&gt;#   @swc/core (postinstall: node postinstall.js)&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: Approve the ones you trust&lt;/span&gt;
npm approve-scripts sharp esbuild @swc/core

&lt;span class="c"&gt;# Step 4: Commit the allowlist&lt;/span&gt;
git add package.json  &lt;span class="c"&gt;# allowScripts field is now populated&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add npm v12 script allowlist"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allowlist lives in &lt;code&gt;package.json&lt;/code&gt; under the &lt;code&gt;allowScripts&lt;/code&gt; key. It's scoped — if a package isn't in the list, its scripts don't run. Commit it. Your CI will pick it up from the committed &lt;code&gt;package.json&lt;/code&gt; with no &lt;code&gt;.npmrc&lt;/code&gt; changes needed.&lt;/p&gt;

&lt;p&gt;But here's the subtlety: &lt;strong&gt;you must run the audit on the same OS and Node version as CI.&lt;/strong&gt; &lt;code&gt;node-gyp rebuild&lt;/code&gt; is conditional — a package might run scripts on Linux but not macOS. Run &lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; in a Docker container that matches your CI environment, or you'll miss scripts that only fire on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CI Pipeline That Passed Green But Shipped Broken Code
&lt;/h2&gt;

&lt;p&gt;We had a GitHub Actions workflow like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three steps passed. The problem: &lt;code&gt;npm ci&lt;/code&gt; skipped &lt;code&gt;sharp&lt;/code&gt;'s postinstall (node-gyp rebuild), and our test suite didn't exercise image processing — it mocked the sharp module. Unit tests passed, but the running app crashed on the first real image upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: add a smoke test after &lt;code&gt;npm ci&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smoke test — native modules&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;node -e "&lt;/span&gt;
      &lt;span class="s"&gt;const sharp = require('sharp');&lt;/span&gt;
      &lt;span class="s"&gt;console.log('sharp loaded:', sharp.versions);&lt;/span&gt;
    &lt;span class="s"&gt;" || (echo 'NATIVE MODULE NOT COMPILED — check allowlist' &amp;amp;&amp;amp; exit 1)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches silent native module failures before the build step. Do this for every native dependency that's critical to your app — sharp, bcrypt, canvas, node-sass, @swc/core, esbuild, prisma, better-sqlite3. One smoke test per native dep, run immediately after &lt;code&gt;npm ci&lt;/code&gt;. Add it to your CI template. The first time it catches a silent skip, you'll thank yourself.&lt;/p&gt;

&lt;p&gt;If you run monorepo CI, put this in a shared workflow or composite action. Every package that touches native deps should have its own smoke test — don't rely on the root &lt;code&gt;npm ci&lt;/code&gt; to catch sub-package failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;npm-shrinkwrap.json&lt;/code&gt; Is Gone — And npm Won't Tell You
&lt;/h2&gt;

&lt;p&gt;If your project uses &lt;code&gt;npm-shrinkwrap.json&lt;/code&gt; (common in CLI tools published to npm), v12 ignores it. Completely. No warning. Your lockfile is just... not the one you think it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# v11: shrinkwrap loaded&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm ci
&lt;span class="c"&gt;# uses npm-shrinkwrap.json for dependency resolution&lt;/span&gt;

&lt;span class="c"&gt;# v12: shrinkwrap silently ignored&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm ci
&lt;span class="c"&gt;# uses package-lock.json (or generates one)&lt;/span&gt;
&lt;span class="c"&gt;# npm-shrinkwrap.json is treated as if it doesn't exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially dangerous for published CLI packages. Your users install your tool, and under v12 they get different dependency versions than what you tested against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: switch to &lt;code&gt;bundleDependencies&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-cli-tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundleDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"yargs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"chalk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ora"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When published with &lt;code&gt;bundleDependencies&lt;/code&gt;, those packages are packed into the tarball. The user installing your CLI gets the exact versions you tested, regardless of npm version — no shrinkwrap needed. This works across npm 11 and 12.&lt;/p&gt;

&lt;p&gt;If you absolutely need shrinkwrap-like behavior without bundling, rename &lt;code&gt;npm-shrinkwrap.json&lt;/code&gt; to &lt;code&gt;package-lock.json&lt;/code&gt; and ensure your publish workflow copies it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;npm-shrinkwrap.json package-lock.json
npm publish &lt;span class="nt"&gt;--lockfile-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But honestly, &lt;code&gt;bundleDependencies&lt;/code&gt; is the better answer for published packages. It's simpler, more portable, and works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;npm view --json&lt;/code&gt; Returns an Array Now
&lt;/h2&gt;

&lt;p&gt;This one broke our release automation script. Every script that parses &lt;code&gt;npm view --json&lt;/code&gt; output needs updating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# v11: single result → object&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm view lodash@4.17.21 version &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="s2"&gt;"4.17.21"&lt;/span&gt;

&lt;span class="c"&gt;# v12: always array, even for single results&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm view lodash@4.17.21 version &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"4.17.21"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our release script was doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;npm view my-package version &lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# v11: echo $LATEST → "1.2.3" (works in jq)&lt;/span&gt;
&lt;span class="c"&gt;# v12: echo $LATEST → ["1.2.3"] (breaks jq parsing)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your CI, Makefile, or release scripts use &lt;code&gt;npm view --json&lt;/code&gt;, grep for it now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"npm view"&lt;/span&gt; .github/ Makefile scripts/ package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each hit, check if it parses the JSON output. Every instance needs the same fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before (v11)&lt;/span&gt;
npm view my-package version &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;

&lt;span class="c"&gt;# After (v12)&lt;/span&gt;
npm view my-package version &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[0]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The array wrapping also hits multi-field queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# v11: single version → single object&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm view lodash@4.17.21 name version &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;:&lt;span class="s2"&gt;"lodash"&lt;/span&gt;,&lt;span class="s2"&gt;"version"&lt;/span&gt;:&lt;span class="s2"&gt;"4.17.21"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# v12: single version → array with one object&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm view lodash@4.17.21 name version &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;:&lt;span class="s2"&gt;"lodash"&lt;/span&gt;,&lt;span class="s2"&gt;"version"&lt;/span&gt;:&lt;span class="s2"&gt;"4.17.21"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your script iterates with &lt;code&gt;jq '.[]'&lt;/code&gt;, it'll still work. If it expects a bare object with &lt;code&gt;jq '.version'&lt;/code&gt;, it breaks. Fix: wrap all your &lt;code&gt;jq&lt;/code&gt; filters in &lt;code&gt;.[0]&lt;/code&gt; or use &lt;code&gt;jq '.[]'&lt;/code&gt; and loop.&lt;/p&gt;

&lt;p&gt;Our full release script patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- PUBLISHED_VERSION=$(npm view my-package version --json | jq -r '.')
&lt;/span&gt;&lt;span class="gi"&gt;+ PUBLISHED_VERSION=$(npm view my-package version --json | jq -r '.[0]')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is easy to miss because it doesn't cause install-time failures. Your release just... silently publishes wrong version strings. Check your scripts before the next release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git Dependencies Are Blocked — And Transitive Ones Count
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;package.json&lt;/code&gt; might not have git deps, but one of your dependencies might. v12 blocks them at every level of the dependency tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This fails in v12&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;some-package
&lt;span class="c"&gt;# Error: Git dependency "github:user/internal-lib" blocked&lt;/span&gt;
&lt;span class="c"&gt;# (comes from some-package → its-dep → git dep, not your direct dep)&lt;/span&gt;

&lt;span class="c"&gt;# What worked in v11:&lt;/span&gt;
&lt;span class="c"&gt;# npm silently resolved and installed the git dep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find hidden git deps before you migrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run while still on npm 11.16.0+&lt;/span&gt;
npm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"(git&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="s2"&gt;|ssh://|git://)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or audit your lockfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"git+"&lt;/span&gt; package-lock.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find transitive git deps, your options, ordered by long-term stability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask the dependency author to publish to npm registry&lt;/strong&gt; — this is the real fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork + publish internally&lt;/strong&gt; — fork the git dep, publish to your private registry or npm, depend on that version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;overrides&lt;/code&gt; in package.json&lt;/strong&gt; — redirect the git dep to a registry version:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"internal-lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:internal-lib-registry@1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allow git globally&lt;/strong&gt; — last resort: &lt;code&gt;allow-git=true&lt;/code&gt; in &lt;code&gt;.npmrc&lt;/code&gt; or &lt;code&gt;--allow-git&lt;/code&gt; flag. Only do this if you control the full dependency chain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Overrides are the fastest fix short-term, but pin a specific version. Floating overrides (&lt;code&gt;"*"&lt;/code&gt;) will bite you on the next upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monorepo-Specific: Workspace Protocol Doesn't Save You
&lt;/h2&gt;

&lt;p&gt;If you use npm workspaces, the &lt;code&gt;workspace:*&lt;/code&gt; protocol doesn't bypass the git/remote blocks. A workspace package that depends on a git dep still fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;packages/&lt;/span&gt;
  &lt;span class="s"&gt;app/         → "deps"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace:*"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="s"&gt;shared/      → "deps"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal-lib"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github:org/internal-lib#v1"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; at the root fails because &lt;code&gt;shared&lt;/code&gt; pulls a git dep transitively. The fix is the same as above — audit all &lt;code&gt;package.json&lt;/code&gt; files in your monorepo, not just the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find packages &lt;span class="nt"&gt;-name&lt;/span&gt; package.json &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"git+"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in CI as a pre-flight check before &lt;code&gt;npm ci&lt;/code&gt;. Block the build if any git deps are found and haven't been explicitly allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 24-Hour Checklist
&lt;/h2&gt;

&lt;p&gt;If you're waking up to npm v12, here's the order of operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your scripts&lt;/strong&gt; — &lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; (run in CI-equivalent Docker)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit the allowlist&lt;/strong&gt; — &lt;code&gt;allowScripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add CI smoke tests&lt;/strong&gt; — one per native dependency, right after &lt;code&gt;npm ci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for shrinkwrap&lt;/strong&gt; — if &lt;code&gt;npm-shrinkwrap.json&lt;/code&gt; exists, migrate to &lt;code&gt;bundleDependencies&lt;/code&gt; or &lt;code&gt;package-lock.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep for &lt;code&gt;npm view --json&lt;/code&gt;&lt;/strong&gt; — update &lt;code&gt;jq&lt;/code&gt; filters from &lt;code&gt;.&lt;/code&gt; to &lt;code&gt;.[0]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit git deps&lt;/strong&gt; — &lt;code&gt;grep -r "git+" package-lock.json&lt;/code&gt;, set overrides or migrate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;npm rebuild&lt;/code&gt; usage&lt;/strong&gt; — it's also subject to script blocking; approve scripts first or pass &lt;code&gt;--allow-scripts&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've been tracking v12 changes since pre.1 and maintain a full migration guide with per-environment configs at &lt;a href="https://installsafe.dev" rel="noopener noreferrer"&gt;installsafe.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>node</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>DiffusionGemma in July 2026: What Works, What Doesn't, and When Ollama Gets It</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:53:28 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/diffusiongemma-in-july-2026-what-works-what-doesnt-and-when-ollama-gets-it-3945</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/diffusiongemma-in-july-2026-what-works-what-doesnt-and-when-ollama-gets-it-3945</guid>
      <description>&lt;p&gt;You've probably seen the benchmarks — 4x faster, 6x more mistakes, 273-vote threads on r/LocalLLaMA. DiffusionGemma is the most interesting image generation model to land this year. But actually running it outside of a HuggingFace notebook is a different story.&lt;/p&gt;

&lt;p&gt;Here's the honest state of every platform as of July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One PR Blocking Everything
&lt;/h2&gt;

&lt;p&gt;Ollama, LM Studio, and native GGUF support all depend on a single pull request: &lt;a href="https://github.com/ggerganov/llama.cpp/pull/24423" rel="noopener noreferrer"&gt;llama.cpp #24423&lt;/a&gt;. It's been open since June 10, initially a proof-of-concept from danielhanchen, now mergeable (conflict resolved). The hold-up is design direction — one maintainer wants a general diffusion server rather than a model-specific implementation. No ETA.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works Today
&lt;/h2&gt;

&lt;h3&gt;
  
  
  vLLM (Recommended)
&lt;/h3&gt;

&lt;p&gt;Day-zero support from the vLLM team. Single GPU works out of the box. Multi-GPU was broken until &lt;a href="https://github.com/vllm-project/vllm/pull/46177" rel="noopener noreferrer"&gt;PR #46177&lt;/a&gt; landed June 26 — tensor parallelism is now in main.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm
vllm serve RedHatAI/diffusiongemma-26B-A4B &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  llama.cpp (Build from PR Branch)
&lt;/h3&gt;

&lt;p&gt;Not in mainline yet. You need to fetch the PR branch and build with CUDA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ggerganov/llama.cpp
&lt;span class="nb"&gt;cd &lt;/span&gt;llama.cpp
git fetch origin pull/24423/head:diffusion-gemma
git checkout diffusion-gemma
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Q4_K_M GGUF available on HuggingFace. Works, but you're on your own for build issues — no guarantees until it lands in main.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollama (Bridge Workaround)
&lt;/h3&gt;

&lt;p&gt;No native &lt;code&gt;ollama pull diffusiongemma&lt;/code&gt; yet. The bridge approach: build llama.cpp from the PR branch, create a custom Modelfile pointing at the GGUF, and serve through Ollama's import mechanism. Clunky but functional. Full steps at &lt;a href="https://diffrun.dev/ollama/" rel="noopener noreferrer"&gt;diffrun.dev/ollama&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doesn't Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LM Studio&lt;/strong&gt; — blocked on #24423. Can't bundle the engine until it's in llama.cpp mainline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GGUF in vLLM&lt;/strong&gt; — &lt;a href="https://github.com/vllm-project/vllm/pull/46454" rel="noopener noreferrer"&gt;PR #46454&lt;/a&gt; fixes GGUF tied embedding loading for DiffusionGemma, mergeable but not merged. Use BF16 weights for now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nvfp4 quantization&lt;/strong&gt; — vLLM's nvfp4 MoE kernels reject DiffusionGemma's expert intermediate size (not a multiple of 128). Separate issue, tracked in the vLLM repo&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Signal That Changes Everything
&lt;/h2&gt;

&lt;p&gt;When #24423 merges, the dominoes fall fast: Ollama ships an engine update within days, LM Studio follows, and native GGUF conversion lands in mainline llama.cpp. Searches for "diffusiongemma ollama" and "diffusiongemma lm studio" will spike overnight.&lt;/p&gt;

&lt;p&gt;I track all three PRs daily at &lt;a href="https://diffrun.dev/status/" rel="noopener noreferrer"&gt;diffrun.dev/status&lt;/a&gt;, with step-by-step setup guides for each platform at &lt;a href="https://diffrun.dev/" rel="noopener noreferrer"&gt;diffrun.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llamacpp</category>
      <category>llm</category>
      <category>ai</category>
      <category>diffusiongemma</category>
    </item>
    <item>
      <title>npm v12 Is Landing in July — Here's Exactly What Will Break and How to Fix It Now</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:47:58 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/npm-v12-is-landing-in-july-heres-exactly-what-will-break-and-how-to-fix-it-now-5h2o</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/npm-v12-is-landing-in-july-heres-exactly-what-will-break-and-how-to-fix-it-now-5h2o</guid>
      <description>&lt;p&gt;TL;DR: npm v12 flips three defaults that will silently break your CI: install scripts are off, Git deps are blocked, remote tarballs are blocked. Your npm ci can exit 0 while native modules were never compiled. Here's a copy-paste migration plan.&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;The Big Picture&lt;/p&gt;

&lt;p&gt;For 12 years, npm has let any package in your dependency tree run arbitrary code during npm install. A single compromised package with a postinstall script could exfiltrate your environment variables, SSH keys, and .npmrc tokens. The Shai Halud supply-chain attack made it impossible to ignore.&lt;/p&gt;

&lt;p&gt;npm v12 finally fixes this. But the fix comes with pain:&lt;/p&gt;

&lt;p&gt;Scripts are off by default — allowScripts defaults to off. Native modules (bcrypt, sharp, esbuild, node-gyp) won't compile.&lt;/p&gt;

&lt;p&gt;Git dependencies are blocked — --allow-git defaults to none. Monorepo internal refs, forks, private repo deps — all blocked.&lt;/p&gt;

&lt;p&gt;Remote URL deps are blocked — --allow-remote defaults to none.&lt;/p&gt;

&lt;p&gt;You will get v12 whether you want it or not. Node.js bundles npm. When LTS releases (22, 24, 26) pick up the v12 bundle, your CI pipelines and dev environments will suddenly have different behavior.&lt;/p&gt;

&lt;p&gt;Good news: npm 11.16.0+ already shows warnings. You can prepare today.&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;The Silent Killer: npm ci Can Succeed While Your App Is Broken&lt;/p&gt;

&lt;p&gt;This is the most dangerous part of v12, and it's catching teams off guard:&lt;/p&gt;

&lt;p&gt;$ npm ci        # exits with code 0 ✅&lt;br&gt;
$ npm test      # passes ✅&lt;br&gt;
$ node app.js   # 💥 Cannot find module 'bcrypt'&lt;/p&gt;

&lt;p&gt;npm v12's npm ci doesn't fail when scripts are blocked — it just silently skips them. Your native modules were never compiled, but CI went green. The app crashes at runtime.&lt;/p&gt;

&lt;p&gt;Fix: Add a smoke test after every npm ci:&lt;/p&gt;

&lt;p&gt;node -e "&lt;br&gt;
const pkgs = ['sharp', 'esbuild', 'bcrypt'];&lt;br&gt;
for (const p of pkgs) {&lt;br&gt;
  try { require(p); console.log('✓', p); }&lt;br&gt;
  catch(e) { console.error('✗', p, '— NOT COMPILED'); process.exit(1); }&lt;br&gt;
}&lt;br&gt;
console.log('All native modules present');&lt;br&gt;
"&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;Migration Plan (15 Minutes)&lt;/p&gt;

&lt;p&gt;Step 1: Upgrade to npm 11.16.0+ (warning mode, nothing breaks yet)&lt;/p&gt;

&lt;p&gt;npm install -g npm@11&lt;/p&gt;

&lt;p&gt;Step 2: See what will be blocked&lt;/p&gt;

&lt;p&gt;npm install&lt;/p&gt;

&lt;p&gt;You'll see warnings like:&lt;/p&gt;

&lt;p&gt;npm warn allow-scripts 8 packages have install scripts not yet covered&lt;br&gt;
npm warn allow-scripts Run &lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; to review&lt;/p&gt;

&lt;p&gt;Step 3: Audit and approve&lt;/p&gt;

&lt;h1&gt;
  
  
  List every package that runs scripts
&lt;/h1&gt;

&lt;p&gt;npm approve-scripts --allow-scripts-pending&lt;/p&gt;

&lt;h1&gt;
  
  
  Approve the ones you trust (most projects have 3-8)
&lt;/h1&gt;

&lt;p&gt;npm approve-scripts bcrypt sharp node-gyp&lt;/p&gt;

&lt;h1&gt;
  
  
  Deny the rest (writes to package.json — commit this)
&lt;/h1&gt;

&lt;p&gt;npm deny-scripts&lt;/p&gt;

&lt;p&gt;Step 4: Find Git dependencies hiding in your tree&lt;/p&gt;

&lt;p&gt;grep -r "git+" package.json package-lock.json&lt;br&gt;
npm ls --all | grep -E "(git+|https://.*.tgz)"&lt;/p&gt;

&lt;p&gt;You might be surprised — transitive dependencies often pull in Git refs. If you find any:&lt;/p&gt;

&lt;h1&gt;
  
  
  Quick fix: add to .npmrc
&lt;/h1&gt;

&lt;p&gt;echo "allow-git=true" &amp;gt;&amp;gt; .npmrc&lt;/p&gt;

&lt;h1&gt;
  
  
  Better fix: migrate to registry versions
&lt;/h1&gt;

&lt;p&gt;Step 5: Check for npm-shrinkwrap.json (it's gone in v12)&lt;/p&gt;

&lt;p&gt;ls npm-shrinkwrap.json 2&amp;gt;/dev/null &amp;amp;&amp;amp; mv npm-shrinkwrap.json package-lock.json&lt;/p&gt;

&lt;p&gt;Step 6: Fix npm view --json parsing&lt;/p&gt;

&lt;p&gt;v12 changes npm view --json to always return arrays. If your CI scripts do:&lt;/p&gt;

&lt;p&gt;npm view lodash version --json | jq -r '.version'&lt;/p&gt;

&lt;p&gt;This will break. Fix:&lt;/p&gt;

&lt;p&gt;npm view lodash version --json | jq -r '.[0]'&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;GitHub Actions: Production-Ready Workflow&lt;/p&gt;

&lt;p&gt;Here's a complete CI workflow for npm v12:&lt;/p&gt;

&lt;p&gt;name: CI — npm v12 Compatible&lt;/p&gt;

&lt;p&gt;on: [push, pull_request]&lt;/p&gt;

&lt;p&gt;jobs:&lt;br&gt;
  test:&lt;br&gt;
    runs-on: ubuntu-latest&lt;br&gt;
    strategy:&lt;br&gt;
      matrix:&lt;br&gt;
        node: [18, 20, 22, 24]&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}

  # Pin npm 12 for forward-compatibility testing
  - run: npm install -g npm@12

  - uses: actions/cache@v4
    with:
      path: ~/.npm
      key: npm-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}

  # Install with scripts pending, then approve known-safe ones
  - run: npm ci --allow-scripts-pending
  - run: npm approve-scripts node-gyp sharp esbuild bcrypt

  # SMOKE TEST: verify native modules actually compiled
  - run: |
      node -e "
      const pkgs = ['sharp', 'esbuild', 'bcrypt'];
      for (const p of pkgs) {
        try { require(p); console.log('✓', p); }
        catch(e) { console.error('✗', p, '— NOT COMPILED'); process.exit(1); }
      }
      "

  - run: npm test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;Docker: Multi-Stage with C++17&lt;/p&gt;

&lt;p&gt;npm v12 requires C++17 for native modules. Your old Docker image with gcc 7 won't cut it.&lt;/p&gt;

&lt;p&gt;FROM node:22-slim&lt;/p&gt;

&lt;h1&gt;
  
  
  npm v12 needs C++17 toolchain
&lt;/h1&gt;

&lt;p&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \&lt;br&gt;
    build-essential python3 make gcc g++ \&lt;br&gt;
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*&lt;/p&gt;

&lt;p&gt;RUN npm install -g npm@12&lt;/p&gt;

&lt;p&gt;WORKDIR /app&lt;br&gt;
COPY package.json package-lock.json ./&lt;/p&gt;

&lt;h1&gt;
  
  
  Install + approve + verify
&lt;/h1&gt;

&lt;p&gt;RUN npm ci --allow-scripts-pending &amp;amp;&amp;amp; \&lt;br&gt;
    npm approve-scripts node-gyp sharp esbuild &amp;amp;&amp;amp; \&lt;br&gt;
    node -e "require('sharp'); require('esbuild'); console.log('Native modules OK')"&lt;/p&gt;

&lt;p&gt;COPY . .&lt;br&gt;
EXPOSE 3000&lt;br&gt;
CMD ["node", "index.js"]&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;Should You Upgrade Now?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pure JS project (no native deps)&lt;/td&gt;
&lt;td&gt;Upgrade now — 5 minute fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has native deps (bcrypt, sharp, etc.)&lt;/td&gt;
&lt;td&gt;Run the audit on npm 11.16.0+ this week, upgrade before July&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo with Git deps&lt;/td&gt;
&lt;td&gt;1 hour audit — find and migrate Git refs first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In the middle of a critical release&lt;/td&gt;
&lt;td&gt;Stay on npm 11, but run the audit so you're ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;Bottom Line&lt;/p&gt;

&lt;p&gt;npm v12 isn't a "should I upgrade?" question. It's a "when will it hit me?" question. Node.js will bundle it, your CI will inherit it, and your deployment will break at the worst possible time — unless you prepare now.&lt;/p&gt;

&lt;p&gt;The migration takes 15 minutes for most projects. The outage it prevents is worth 15 hours.&lt;/p&gt;

&lt;p&gt;───&lt;/p&gt;

&lt;p&gt;This guide is based on npm 11.16.0+ behavior and the official npm v12 announcement. For more details, check out InstallSafe.dev — a free npm v12 migration guide with FAQ, CI/CD templates, and a side-by-side npm 11 vs 12 comparison.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Replaced NotebookLM with a Self-Hosted Alternative (After 2 Weeks of Daily Use)</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Mon, 29 Jun 2026 08:29:23 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/why-i-replaced-notebooklm-with-a-self-hosted-alternative-after-2-weeks-of-daily-use-3oof</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/why-i-replaced-notebooklm-with-a-self-hosted-alternative-after-2-weeks-of-daily-use-3oof</guid>
      <description>&lt;h1&gt;
  
  
  Why I Replaced NotebookLM with a Self-Hosted Alternative (After 2 Weeks of Daily Use)
&lt;/h1&gt;

&lt;p&gt;I've been a heavy Google NotebookLM user for over a year. The audio overviews, the document chat, the research notebooks — it's genuinely great software. But after hitting its limits one too many times, I finally switched. Here's what pushed me over the edge, and why I'm not going back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Dealbreakers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "Source limit reached" — again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I was researching a complex topic for a client project and needed to load about 80 sources — PDFs, YouTube transcripts, release notes, API docs. NotebookLM told me I'd hit the 50-source cap. So I deleted some older notes to make room. Then I hit the 500K word limit. This is the worst kind of friction — you're in the middle of deep work, and the tool yanks you out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Wait, my proprietary design docs are on Google's servers?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one hit me during a late-night work session. I'd uploaded internal architecture documents into NotebookLM to generate a podcast overview for the team. Then I realized: all of this — our intellectual property — is sitting on Google's infrastructure with no option to keep it local. For a personal study guide, fine. For actual work? That's a harder conversation with legal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Gemini-only lock-in&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NotebookLM is excellent — but it's excellent &lt;em&gt;at what Gemini can do&lt;/em&gt;. When Gemini misinterprets technical jargon, there's no "retry with Claude." When the audio overviews sound robotic, there's no "generate with ElevenLabs." You get what Google gives you, end of story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Open Notebook
&lt;/h2&gt;

&lt;p&gt;I found &lt;a href="https://localnotebook.dev" rel="noopener noreferrer"&gt;Open Notebook&lt;/a&gt; (formerly Open Notebook LM) while searching for "self-hosted notebooklm alternative" on GitHub. 31K stars. MIT license. Docker setup in two minutes. I was skeptical but figured I'd give it a weekend test.&lt;/p&gt;

&lt;p&gt;That weekend turned into two weeks of daily use. Here's what sold me:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Docker in 2 minutes. I'm not exaggerating.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;open-notebook &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;open-notebook
curl &lt;span class="nt"&gt;-o&lt;/span&gt; docker-compose.yml https://raw.githubusercontent.com/lfnovo/open-notebook/main/docker-compose.yml
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/change-me-to-a-secret-string/YOUR-ENCRYPTION-KEY/'&lt;/span&gt; docker-compose.yml
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browse to &lt;code&gt;localhost:8502&lt;/code&gt;, add your API keys in the Models panel, and you're working. It genuinely takes less time than ordering coffee.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model routing is a superpower I didn't know I needed
&lt;/h3&gt;

&lt;p&gt;This is the feature I use the most. Open Notebook lets you assign different AI models to different tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summaries&lt;/strong&gt; → GPT-4o-mini (cheap, fast)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep analysis&lt;/strong&gt; → Claude 3.5 Sonnet or Opus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private/internal docs&lt;/strong&gt; → Local Ollama with Qwen 3.6 or Mistral (data never leaves your machine)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; → text-embedding-3-small (or qwen3-embedding for zero-cost local embeddings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My API bill for two weeks of heavy use: $6.37. That's less than a NotebookLM Plus monthly sub, and I processed about 3x more content.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-speaker podcasts that actually sound different
&lt;/h3&gt;

&lt;p&gt;NotebookLM gives you two AI hosts doing a "deep dive" conversation — which is great until you've heard the same format 50 times. Open Notebook supports 1 to 4 speaker profiles with customizable voices and persona prompts. I can set one speaker as "skeptical technical reviewer," another as "domain expert," and the third as "curious newcomer" — and the resulting conversation actually reflects those perspectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. REST API on port 5055
&lt;/h3&gt;

&lt;p&gt;This is the programmable angle. Need to batch-process 200 PDFs overnight? &lt;code&gt;curl&lt;/code&gt; to the API. Want to generate weekly podcasts from a knowledge base automatically? Script it. Something NotebookLM simply cannot do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5055/api/notebook/process &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"source": "repository", "action": "generate_podcast"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Trade-offs
&lt;/h2&gt;

&lt;p&gt;I won't pretend it's all roses. Here's where NotebookLM still wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Citations&lt;/strong&gt; — NotebookLM's inline citations with exact passage highlighting are much better. Open Notebook gives basic references, but they're working on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-setup polish&lt;/strong&gt; — NotebookLM works the moment you sign in. Open Notebook requires Docker and API keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app&lt;/strong&gt; — NotebookLM has native apps. Open Notebook's UI works on mobile browsers but isn't optimized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for me — a developer working with sensitive documents who needs more than 50 sources and wants to choose my models — these trade-offs are worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Switch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Developers who hit NotebookLM's source limits regularly&lt;/li&gt;
&lt;li&gt;Anyone working with proprietary/confidential documents&lt;/li&gt;
&lt;li&gt;Teams who want to choose their AI providers and control costs&lt;/li&gt;
&lt;li&gt;People who need to automate document processing via API&lt;/li&gt;
&lt;li&gt;Anyone who wants to self-host for data sovereignty&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Should Stay with NotebookLM
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Casual users with fewer than 50 documents&lt;/li&gt;
&lt;li&gt;People who need citation-perfect research&lt;/li&gt;
&lt;li&gt;Anyone who doesn't want to touch a terminal or Docker&lt;/li&gt;
&lt;li&gt;Users who value simplicity over control&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;After two weeks, I've kept both. NotebookLM for quick public-research throwaway stuff. Open Notebook for everything that matters — client work, internal docs, anything I wouldn't paste into a Google textbox.&lt;/p&gt;

&lt;p&gt;If you're a developer who's been frustrated by NotebookLM's limits, give &lt;a href="https://localnotebook.dev" rel="noopener noreferrer"&gt;Open Notebook&lt;/a&gt; a spin. Docker setup takes two minutes. What you do with it after that is up to you.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://localnotebook.dev" rel="noopener noreferrer"&gt;https://localnotebook.dev&lt;/a&gt;&lt;/strong&gt; — Self-hosted NotebookLM alternative, 31K+ GitHub stars, MIT license.&lt;/p&gt;

</description>
      <category>notebooklm</category>
      <category>selfhosting</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Tested 5 Open-Source NotebookLM Alternatives — Here's What Actually Works</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Sun, 28 Jun 2026 17:59:19 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-tested-5-open-source-notebooklm-alternatives-heres-what-actually-works-23em</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-tested-5-open-source-notebooklm-alternatives-heres-what-actually-works-23em</guid>
      <description>&lt;p&gt;Google's NotebookLM is great. But handing your research notes, PDFs, and meeting transcripts to Google's cloud is a hard sell for a lot of people — especially when those documents contain client data, unpublished research, or internal strategy.&lt;/p&gt;

&lt;p&gt;So I spent a weekend testing five open-source alternatives. Three things mattered: &lt;strong&gt;can I &lt;code&gt;docker compose up&lt;/code&gt; in under 10 minutes, does the podcast feature actually work offline, and what breaks first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contenders
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Deploy Time&lt;/th&gt;
&lt;th&gt;Min VRAM&lt;/th&gt;
&lt;th&gt;True Offline&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Open Notebook&lt;/strong&gt; (lfnovo)&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Notex&lt;/strong&gt; (smallnest)&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;KnowNote&lt;/strong&gt; (MrSibe)&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;NotebookLM-Local&lt;/strong&gt; (nagaforcloud)&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Qwen-3 4B bundled&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;InsightsLM&lt;/strong&gt; (phsphd)&lt;/td&gt;
&lt;td&gt;~30 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N8N SUS license&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Open Notebook — The One to Beat
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lfnovo/open-notebook
&lt;span class="nb"&gt;cd &lt;/span&gt;open-notebook
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight minutes from &lt;code&gt;git clone&lt;/code&gt; to the web UI on &lt;code&gt;localhost:3000&lt;/code&gt;. It ships with 18+ model providers pre-configured — Ollama, OpenAI, Claude, DeepSeek, Gemini, all selectable per notebook. The podcast generator supports 1-4 speakers with different voices, and it runs &lt;strong&gt;entirely offline&lt;/strong&gt; when you point it at an Ollama backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document ingestion is fast — SurrealDB's vector + full-text index handles 200-page PDFs without choking&lt;/li&gt;
&lt;li&gt;Model switching is genuinely useful — Claude for deep analysis on one notebook, local Qwen for quick summaries on another&lt;/li&gt;
&lt;li&gt;Podcast quality with 2 speakers is close to NotebookLM's. 4 speakers is still rough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Citation highlighting is still being rebuilt (work in progress as of June 2026)&lt;/li&gt;
&lt;li&gt;Single-user only — no team/workspace isolation built in&lt;/li&gt;
&lt;li&gt;Docker required. No native binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Notex — Single Binary, Zero Dependencies
&lt;/h2&gt;

&lt;p&gt;Notex is written in Go. You download a single binary (~25MB) and run &lt;code&gt;./notex&lt;/code&gt;. That's it. No Docker, no Python venv, no database setup. It supports PDF, TXT, MD, DOCX, HTML, audio, and YouTube/Bilibili URLs as sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; people who want the absolute minimum infrastructure. If Docker is a dealbreaker, Notex is your pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still v0.3.1 — the podcast feature generates a script, not audio. You'll need a separate TTS step.&lt;/li&gt;
&lt;li&gt;Smaller community, fewer config examples floating around.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. KnowNote — Desktop App, No Server
&lt;/h2&gt;

&lt;p&gt;Electron app with SQLite storage. Download, install, open — no terminal needed. Cross-platform (Windows + macOS). If you're setting this up for a non-technical colleague who just wants a "NotebookLM on my laptop," this is the closest to that experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; solo users who want zero setup and don't need a web interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop-only — no API, no server mode, no sharing.&lt;/li&gt;
&lt;li&gt;Early stage — rough edges in the UI, occasional Electron quirks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. NotebookLM-Local — The Truly Offline Option
&lt;/h2&gt;

&lt;p&gt;Bundles Qwen-3 4B directly so it runs 100% offline with zero API keys. Uses llama.cpp with Metal acceleration for Apple Silicon. The flat-file vector store means no database to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; privacy maximalists who want everything on-device and don't mind slower inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen-3 4B is not Claude. Summaries are functional but shallow compared to cloud models.&lt;/li&gt;
&lt;li&gt;macOS/Apple Silicon recommended. CPU-only on Linux works but is very slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. InsightsLM — Powerful but Heavy
&lt;/h2&gt;

&lt;p&gt;Built on Supabase + N8N + React. The no-code workflow engine (N8N) means you can build custom document processing pipelines, auto-tagging, webhook triggers — things the others can't do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need programmable document workflows, not just a notebook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup takes 30+ minutes with multiple services. This is not "docker compose up and done."&lt;/li&gt;
&lt;li&gt;N8N's Sustainable Use License is not fully open-source for commercial SaaS use. Read the license before building a product on top of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What None of Them Handle Well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Production hardening.&lt;/strong&gt; All five are dev-server default configs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No HTTPS out of the box&lt;/li&gt;
&lt;li&gt;No authentication (single-user, exposed to localhost only)&lt;/li&gt;
&lt;li&gt;No backup automation&lt;/li&gt;
&lt;li&gt;No monitoring or health checks&lt;/li&gt;
&lt;li&gt;No CI/CD for model updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're just exploring for personal use, the defaults are fine. If you're sharing it with your team or depending on it for daily research, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nginx reverse proxy with TLS (Let's Encrypt automated renewal)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restart: unless-stopped&lt;/code&gt; + health checks that verify the model is loaded, not just the port&lt;/li&gt;
&lt;li&gt;Automated daily backups with off-site storage (B2/S3)&lt;/li&gt;
&lt;li&gt;Per-team instance isolation if you have multiple groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote up the full production deployment flow at &lt;a href="https://localnotebook.dev" rel="noopener noreferrer"&gt;localnotebook.dev&lt;/a&gt; — the free deployment guide covers getting started, and the Production Manual covers the hardening steps above (10 chapters, Docker Compose → monitoring → 30+ error fixes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want one &lt;code&gt;docker compose up&lt;/code&gt; and done?&lt;/strong&gt; → Open Notebook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker is a dealbreaker?&lt;/strong&gt; → Notex&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting this up for a non-technical person?&lt;/strong&gt; → KnowNote&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API keys, no cloud, period?&lt;/strong&gt; → NotebookLM-Local&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need programmable workflows?&lt;/strong&gt; → InsightsLM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All five are better than uploading your research to Google. Pick based on your tolerance for Docker and how much you care about podcast quality.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This comparison was done in June 2026. Projects are moving fast — especially Open Notebook which ships updates weekly. Check their repos for the latest.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>ai</category>
      <category>notebooklm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Tested 5 Open-Source NotebookLM Alternatives — Here's What Actually Works</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:33:22 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-tested-5-open-source-notebooklm-alternatives-heres-what-actually-works-12bc</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-tested-5-open-source-notebooklm-alternatives-heres-what-actually-works-12bc</guid>
      <description>&lt;p&gt;Google's NotebookLM is great. But handing your research notes, PDFs, and meeting transcripts to Google's cloud is a hard sell for a lot of people — especially when those documents contain client data, unpublished research, or internal strategy.&lt;/p&gt;

&lt;p&gt;So I spent a weekend testing five open-source alternatives. Three things mattered: &lt;strong&gt;can I &lt;code&gt;docker compose up&lt;/code&gt; in under 10 minutes, does the podcast feature actually work offline, and what breaks first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Contenders
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Deploy Time&lt;/th&gt;
&lt;th&gt;Min VRAM&lt;/th&gt;
&lt;th&gt;True Offline&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Open Notebook&lt;/strong&gt; (lfnovo)&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Notex&lt;/strong&gt; (smallnest)&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;KnowNote&lt;/strong&gt; (MrSibe)&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;NotebookLM-Local&lt;/strong&gt; (nagaforcloud)&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Qwen-3 4B bundled&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;InsightsLM&lt;/strong&gt; (phsphd)&lt;/td&gt;
&lt;td&gt;~30 min&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N8N SUS license&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Open Notebook — The One to Beat
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lfnovo/open-notebook
&lt;span class="nb"&gt;cd &lt;/span&gt;open-notebook
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight minutes from &lt;code&gt;git clone&lt;/code&gt; to the web UI on &lt;code&gt;localhost:3000&lt;/code&gt;. It ships with 18+ model providers pre-configured — Ollama, OpenAI, Claude, DeepSeek, Gemini, all selectable per notebook. The podcast generator supports 1-4 speakers with different voices, and it runs &lt;strong&gt;entirely offline&lt;/strong&gt; when you point it at an Ollama backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document ingestion is fast — SurrealDB's vector + full-text index handles 200-page PDFs without choking&lt;/li&gt;
&lt;li&gt;Model switching is genuinely useful — Claude for deep analysis on one notebook, local Qwen for quick summaries on another&lt;/li&gt;
&lt;li&gt;Podcast quality with 2 speakers is close to NotebookLM's. 4 speakers is still rough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Citation highlighting is still being rebuilt (work in progress as of June 2026)&lt;/li&gt;
&lt;li&gt;Single-user only — no team/workspace isolation built in&lt;/li&gt;
&lt;li&gt;Docker required. No native binary.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Notex — Single Binary, Zero Dependencies
&lt;/h2&gt;

&lt;p&gt;Notex is written in Go. You download a single binary (~25MB) and run &lt;code&gt;./notex&lt;/code&gt;. That's it. No Docker, no Python venv, no database setup. It supports PDF, TXT, MD, DOCX, HTML, audio, and YouTube/Bilibili URLs as sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; people who want the absolute minimum infrastructure. If Docker is a dealbreaker, Notex is your pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still v0.3.1 — the podcast feature generates a script, not audio. You'll need a separate TTS step.&lt;/li&gt;
&lt;li&gt;Smaller community, fewer config examples floating around.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. KnowNote — Desktop App, No Server
&lt;/h2&gt;

&lt;p&gt;Electron app with SQLite storage. Download, install, open — no terminal needed. Cross-platform (Windows + macOS). If you're setting this up for a non-technical colleague who just wants a "NotebookLM on my laptop," this is the closest to that experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; solo users who want zero setup and don't need a web interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop-only — no API, no server mode, no sharing.&lt;/li&gt;
&lt;li&gt;Early stage — rough edges in the UI, occasional Electron quirks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. NotebookLM-Local — The Truly Offline Option
&lt;/h2&gt;

&lt;p&gt;Bundles Qwen-3 4B directly so it runs 100% offline with zero API keys. Uses llama.cpp with Metal acceleration for Apple Silicon. The flat-file vector store means no database to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; privacy maximalists who want everything on-device and don't mind slower inference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen-3 4B is not Claude. Summaries are functional but shallow compared to cloud models.&lt;/li&gt;
&lt;li&gt;macOS/Apple Silicon recommended. CPU-only on Linux works but is very slow.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. InsightsLM — Powerful but Heavy
&lt;/h2&gt;

&lt;p&gt;Built on Supabase + N8N + React. The no-code workflow engine (N8N) means you can build custom document processing pipelines, auto-tagging, webhook triggers — things the others can't do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need programmable document workflows, not just a notebook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup takes 30+ minutes with multiple services. This is not "docker compose up and done."&lt;/li&gt;
&lt;li&gt;N8N's Sustainable Use License is not fully open-source for commercial SaaS use. Read the license before building a product on top of it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What None of Them Handle Well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Production hardening.&lt;/strong&gt; All five are dev-server default configs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No HTTPS out of the box&lt;/li&gt;
&lt;li&gt;No authentication (single-user, exposed to localhost only)&lt;/li&gt;
&lt;li&gt;No backup automation&lt;/li&gt;
&lt;li&gt;No monitoring or health checks&lt;/li&gt;
&lt;li&gt;No CI/CD for model updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're just exploring for personal use, the defaults are fine. If you're sharing it with your team or depending on it for daily research, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nginx reverse proxy with TLS (Let's Encrypt automated renewal)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restart: unless-stopped&lt;/code&gt; + health checks that verify the model is loaded, not just the port&lt;/li&gt;
&lt;li&gt;Automated daily backups with off-site storage (B2/S3)&lt;/li&gt;
&lt;li&gt;Per-team instance isolation if you have multiple groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote up the full production deployment flow at &lt;a href="https://localnotebook.dev" rel="noopener noreferrer"&gt;localnotebook.dev&lt;/a&gt; — the free deployment guide covers getting started, and the Production Manual covers the hardening steps above (10 chapters, Docker Compose → monitoring → 30+ error fixes).&lt;/p&gt;




&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want one &lt;code&gt;docker compose up&lt;/code&gt; and done?&lt;/strong&gt; → Open Notebook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker is a dealbreaker?&lt;/strong&gt; → Notex&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting this up for a non-technical person?&lt;/strong&gt; → KnowNote&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API keys, no cloud, period?&lt;/strong&gt; → NotebookLM-Local&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need programmable workflows?&lt;/strong&gt; → InsightsLM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All five are better than uploading your research to Google. Pick based on your tolerance for Docker and how much you care about podcast quality.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This comparison was done in June 2026. Projects are moving fast — especially Open Notebook which ships updates weekly. Check their repos for the latest.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>ai</category>
      <category>notebooklm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Beyond pip install: MarkItDown in Production</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Thu, 11 Jun 2026 04:39:07 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/beyond-pip-install-markitdown-in-production-g0b</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/beyond-pip-install-markitdown-in-production-g0b</guid>
      <description>&lt;h1&gt;
  
  
  Beyond pip install: MarkItDown in Production
&lt;/h1&gt;

&lt;p&gt;MarkItDown is Microsoft's open-source library for converting documents to Markdown. A single &lt;code&gt;pip install markitdown&lt;/code&gt; and you're converting DOCX, PDF, PPTX, and XLSX files in seconds.&lt;/p&gt;

&lt;p&gt;But between "it works on my machine" and "it works in production," there's a gap the official docs don't cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Breaks in Production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures:&lt;/strong&gt; Encrypted PDFs return empty strings — no error, no warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No timeout:&lt;/strong&gt; Large PDFs can hang your pipeline with no way to cancel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table scrambling:&lt;/strong&gt; Merged cells and complex layouts lose structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF noise:&lt;/strong&gt; CID markers, duplicate sentences, zero heading hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency fragility:&lt;/strong&gt; Unpinned versions can silently break&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Helps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing:&lt;/strong&gt; Reuse a single &lt;code&gt;MarkItDown()&lt;/code&gt; instance across hundreds of files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker + FastAPI:&lt;/strong&gt; Production-ready API with file size limits and timeout handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF cleanup pipeline:&lt;/strong&gt; Python script that strips noise, deduplicates, and restores structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right LLM for images:&lt;/strong&gt; Claude 4 Sonnet wins on detail, GPT-4o wins on chart accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MCP Server Integration
&lt;/h2&gt;

&lt;p&gt;Turn MarkItDown into a Claude Desktop tool — paste a file path in chat and get clean Markdown instantly. No terminal, no scripts.&lt;/p&gt;

&lt;p&gt;Full &lt;a href="https://markitdown-pro.com/projects/markitdown/" rel="noopener noreferrer"&gt;production guide with code, Docker setup, and LLM comparison&lt;/a&gt;. Also see the &lt;a href="https://markitdown-pro.com/projects/markitdown/vs-unstructured/" rel="noopener noreferrer"&gt;MarkItDown vs Unstructured vs LlamaParse comparison&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>MoneyPrinterTurbo — 72K GitHub Stars, Is It Actually Usable?</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Thu, 11 Jun 2026 04:37:46 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/moneyprinterturbo-72k-github-stars-is-it-actually-usable-lim</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/moneyprinterturbo-72k-github-stars-is-it-actually-usable-lim</guid>
      <description>&lt;h1&gt;
  
  
  MoneyPrinterTurbo — 72K GitHub Stars, Is It Actually Usable?
&lt;/h1&gt;

&lt;p&gt;When a GitHub repo hits 72,000 stars by promising "one-click AI video generation," it's worth a closer look. But stars don't tell the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;MPT takes a topic, generates a script via LLM, creates voiceover with TTS, pulls stock footage from Pexels, and stitches everything together with subtitles and background music. The result is a polished slideshow-style video — not Sora-style AI generation, but good enough for faceless YouTube channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Hype Doesn't Tell You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latest version has no GUI.&lt;/strong&gt; v1.2.9 is API-only. Beginners need v1.1.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two terminal windows.&lt;/strong&gt; Backend (FastAPI) + frontend (Streamlit) run separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies break.&lt;/strong&gt; moviepy 2.0 kills the install. edge-tts renamed half its API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free AI providers die.&lt;/strong&gt; The g4f config everyone recommends no longer works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per video:&lt;/strong&gt; $0 (g4f + Edge TTS + Pexels)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to produce 1-min video:&lt;/strong&gt; ~60 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup time (reality):&lt;/strong&gt; 2–4 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;For faceless content channels at zero cost, it's unbeatable. Just don't expect a smooth setup. Follow a guide that covers the real pitfalls.&lt;/p&gt;

&lt;p&gt;Full &lt;a href="https://mpt-tutorial.com/projects/mpt/" rel="noopener noreferrer"&gt;MoneyPrinterTurbo review with 18 documented bugs and fixes&lt;/a&gt;, plus a &lt;a href="https://mpt-tutorial.com/projects/mpt/setup-guide/" rel="noopener noreferrer"&gt;10-minute setup guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>DiffusionGemma: Run Google's 4x Faster Diffusion LLM Locally (Full Setup Guide)</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Thu, 11 Jun 2026 02:40:03 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/diffusiongemma-run-googles-4x-faster-diffusion-llm-locally-full-setup-guide-4cd</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/diffusiongemma-run-googles-4x-faster-diffusion-llm-locally-full-setup-guide-4cd</guid>
      <description>&lt;h1&gt;
  
  
  DiffusionGemma: Run Google's 4x Faster Diffusion LLM Locally
&lt;/h1&gt;

&lt;p&gt;Google DeepMind just open-sourced &lt;strong&gt;DiffusionGemma&lt;/strong&gt; — and it's not just another Gemma model. It's a fundamentally different way to generate text: diffusion instead of autoregression.&lt;/p&gt;

&lt;p&gt;Here's what you need to know to run it on your own machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes It Different
&lt;/h2&gt;

&lt;p&gt;Standard LLMs (GPT, Llama, Qwen) generate text one token at a time, left-to-right. Each token depends on all previous tokens, and once it's generated, it's permanent.&lt;/p&gt;

&lt;p&gt;DiffusionGemma works like a text version of Stable Diffusion: it fills in &lt;strong&gt;256 tokens at once&lt;/strong&gt; through iterative denoising. Every token in that block can attend to every other token. If the model loses confidence in a token mid-generation, it can go back and fix it — something autoregressive models fundamentally cannot do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;26B parameters total, 3.8B activated (Mixture of Experts)&lt;/li&gt;
&lt;li&gt;256K context window, 140+ languages&lt;/li&gt;
&lt;li&gt;Apache 2.0 license — truly open, no usage restrictions&lt;/li&gt;
&lt;li&gt;Multimodal: text + image + video inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Speed Difference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Quantization&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H200&lt;/td&gt;
&lt;td&gt;1,288&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;vLLM (verified)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;~1,000+&lt;/td&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;Google (self-reported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;~700+&lt;/td&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;Google (self-reported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4090&lt;/td&gt;
&lt;td&gt;~200-400&lt;/td&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;Community estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;⚡️ This is a fundamentally different speed tier from autoregressive models — a 26B-class model running at 700+ tokens/s on a single GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 Ways to Run It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: vLLM (Fastest)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;0.12.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamplingParams&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/diffusiongemma-26B-A4B-it&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;trust_remote_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tensor_parallel_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_model_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sampling_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain how diffusion language models work.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sampling_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vLLM has day-zero optimized support with dedicated block-denoising kernels. This is the gold standard for production inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: llama.cpp (For GGUF quantization)
&lt;/h3&gt;

&lt;p&gt;llama.cpp support is via &lt;strong&gt;PR #24427&lt;/strong&gt; — not merged yet, but functional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ggml-ai/llama.cpp
&lt;span class="nb"&gt;cd &lt;/span&gt;llama.cpp
git fetch origin pull/24427/head:diffusiongemma
git checkout diffusiongemma
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;--config&lt;/span&gt; Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then download a GGUF quantized weight and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./build/bin/llama-cli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; DiffusionGemma-26B-A4B-it-Q4_K_M.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Explain how diffusion language models work."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; 512 &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Method 3: HuggingFace Transformers (Simplest)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;transformers&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;4.55.0 accelerate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/diffusiongemma-26B-A4B-it&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;trust_remote_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quantization Guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4&lt;/td&gt;
&lt;td&gt;~15GB&lt;/td&gt;
&lt;td&gt;Near-lossless&lt;/td&gt;
&lt;td&gt;RTX 5090 / Blackwell only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitsandbytes 4-bit&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;RTX 3090/4090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GGUF Q4_K_M&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;llama.cpp users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;~52GB&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;H100/A100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it's great at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-form generation (1-2 paragraphs): quality competitive with Gemma 4&lt;/li&gt;
&lt;li&gt;Summarization and translation (140+ languages)&lt;/li&gt;
&lt;li&gt;Data augmentation / synthetic text: throughput &amp;gt;&amp;gt; autoregressive&lt;/li&gt;
&lt;li&gt;Real-time interactive applications: sub-100ms latency feels instant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex reasoning (math, logic, multi-hop): significantly below Gemma 4&lt;/li&gt;
&lt;li&gt;Long-form writing (3+ paragraphs): coherence degrades after block boundaries&lt;/li&gt;
&lt;li&gt;Code generation: functional for snippets, struggles with multi-file code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardware caveat:&lt;/strong&gt; The speed advantage requires high-compute GPUs (RTX 3090+). Apple Silicon Macs and entry-level GPUs won't see the 4x speedup — Google explicitly warns about this in their blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Try It?
&lt;/h2&gt;

&lt;p&gt;✅ &lt;strong&gt;Yes&lt;/strong&gt;, if you have RTX 4090/5090 and need high-throughput generation&lt;br&gt;
✅ &lt;strong&gt;Yes&lt;/strong&gt;, if you're generating synthetic data at scale&lt;br&gt;
✅ &lt;strong&gt;Yes&lt;/strong&gt;, if you're curious about diffusion LLMs as a research direction&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;No&lt;/strong&gt;, if you need top-tier reasoning or code quality&lt;br&gt;
❌ &lt;strong&gt;No&lt;/strong&gt;, if you're on Apple Silicon (the speed advantage evaporates)&lt;br&gt;
❌ &lt;strong&gt;No&lt;/strong&gt;, if you need Ollama support (not available yet)&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Guide
&lt;/h2&gt;

&lt;p&gt;I built a complete deployment guide with hardware requirements, troubleshooting, and honest benchmarks at &lt;strong&gt;&lt;a href="https://diffusiongemma.dev" rel="noopener noreferrer"&gt;diffusiongemma.dev&lt;/a&gt;&lt;/strong&gt; — 4 deployment methods with copy-paste commands.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published 2026-06-11. Model released by Google DeepMind under Apache 2.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a World Cup 2026 Interactive Venue Map with Leaflet.js — Here's the Code</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Tue, 09 Jun 2026 08:32:10 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-built-a-world-cup-2026-interactive-venue-map-with-leafletjs-heres-the-code-574d</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/i-built-a-world-cup-2026-interactive-venue-map-with-leafletjs-heres-the-code-574d</guid>
      <description>&lt;h1&gt;
  
  
  I Built a World Cup 2026 Interactive Venue Map with Leaflet.js — Here's the Code
&lt;/h1&gt;

&lt;p&gt;World Cup 2026 kicks off in 3 days. 48 teams, 16 stadiums across the US, Canada, and Mexico. I wanted to see where all the matches were happening — not on a static image, not in a slow third-party iframe, but on a fast, zoomable, clickable map that I actually built myself.&lt;/p&gt;

&lt;p&gt;So I spent an afternoon with Leaflet.js and Cloudflare Pages. Here's what came out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Another Map?
&lt;/h2&gt;

&lt;p&gt;I searched "World Cup 2026 stadiums map" and found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Mapme embed that took 4 seconds to load&lt;/li&gt;
&lt;li&gt;A ZeeMaps link with ads all over it&lt;/li&gt;
&lt;li&gt;A bunch of JPG images with tiny text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them let you click a stadium and see all the matches happening there. None were fast. None felt like they were built by someone who actually wanted to use them.&lt;/p&gt;

&lt;p&gt;So I built my own. The goal: load in under 500ms, run on a phone, and let you tap any stadium to see every match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack: Leaflet.js, JSON, and Nothing Else
&lt;/h2&gt;

&lt;p&gt;No React. No Next.js. No API. No backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two CDN links, a single HTML file, a JSON blob of stadiums and matches, and Cloudflare Pages to serve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stadium Data
&lt;/h2&gt;

&lt;p&gt;I structured everything as a flat JSON array. Each stadium has coordinates, capacity, and its match list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stadiums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MetLife Stadium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;East Rutherford, NJ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;40.8136&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;74.0744&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;82500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-19&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15:00 ET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-13&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Group C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Brazil vs Morocco&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-22&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Group I&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Norway vs Senegal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Round of 32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TBD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Round of 16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TBD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 15 more stadiums&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON is about 4KB. That's the entire data layer. No database, no API calls, nothing to go down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Map Init
&lt;/h2&gt;

&lt;p&gt;Leaflet makes this embarrassingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;39.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;98.5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tileLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;copy; OpenStreetMap contributors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxZoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Centered on middle America at zoom 4 so all three countries fit on screen. CartoDB's light tiles keep it clean — no visual noise competing with the stadium markers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the Markers
&lt;/h2&gt;

&lt;p&gt;For each stadium, I create a custom marker with a football emoji and bind a popup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;stadiums&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stadium&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;divIcon&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚽&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stadium-marker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iconSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;iconAnchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stadium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;lt;div class="match-row"&amp;gt;
      &amp;lt;span class="match-date"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
      &amp;lt;span class="match-stage"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
      &amp;lt;span class="match-teams"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stadium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;div class="stadium-popup"&amp;gt;
        &amp;lt;h3&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stadium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h3&amp;gt;
        &amp;lt;p class="stadium-location"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stadium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stadium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; seats&amp;lt;/p&amp;gt;
        &amp;lt;div class="match-schedule"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matchList&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;
        &amp;lt;a class="popup-link" href="https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/scores-fixtures" target="_blank"&amp;gt;
          Full schedule →
        &amp;lt;/a&amp;gt;
      &amp;lt;/div&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;divIcon&lt;/code&gt; with an emoji is a cheat code — no custom image assets, no sprite sheet, just one character that renders perfectly at any zoom level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile Matters
&lt;/h2&gt;

&lt;p&gt;The one CSS tweak that made a real difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.stadium-marker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;28px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-tap-highlight-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.leaflet-popup-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;320px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.match-row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On desktop, it's a map. On a phone, it's still a functional tap-to-explore experience. The 32×32px tap target on markers is right at the recommended minimum — I didn't want to inflate them and lose precision when markers are close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;You don't need a framework to ship a useful interactive tool in an afternoon.&lt;/p&gt;

&lt;p&gt;The entire project is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 HTML file (147 lines)&lt;/li&gt;
&lt;li&gt;1 JSON data file (4KB)&lt;/li&gt;
&lt;li&gt;2 CDN links&lt;/li&gt;
&lt;li&gt;Deployed to Cloudflare Pages in 30 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No build step. No &lt;code&gt;npm install&lt;/code&gt;. No state management to debug. Just a map that loads fast and does exactly one thing well.&lt;/p&gt;

&lt;p&gt;The real lesson: Leaflet.js in 2026 is still the best tool for this kind of project. It hasn't had a major version bump in years, and that's a feature — the API is stable, the docs are complete, and it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;I've been on a kick lately of building lightweight, single-purpose tools. If you're into that kind of thing, I've also been writing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TokenCut&lt;/strong&gt; — practical ways to cut your AI coding token costs by 30-50%, including a breakdown of why Markdown is 3-8x more token-efficient than HTML for LLM pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown Pro&lt;/strong&gt; — batch-converting hundreds of documents to clean Markdown for LLM ingestion, using Microsoft's open-source converter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: build small, ship fast, and let the tool do one thing really well.&lt;/p&gt;

&lt;p&gt;World Cup kicks off June 11. If you want to build your own map, grab the full code from the companion repo below. You've got 3 days — that's 2.5 more than you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Jakeshadow/world-cup-venue-map" rel="noopener noreferrer"&gt;github.com/Jakeshadow/world-cup-venue-map&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live map:&lt;/strong&gt; &lt;a href="https://wc26-map.pages.dev" rel="noopener noreferrer"&gt;wc26-map.pages.dev&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;More tools:&lt;/strong&gt; &lt;a href="https://tokencut.org" rel="noopener noreferrer"&gt;tokencut.org&lt;/a&gt; · &lt;a href="https://markitdown-pro.com" rel="noopener noreferrer"&gt;markitdown-pro.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>leaflet</category>
      <category>datavis</category>
    </item>
    <item>
      <title>How I Batch-Convert 100+ Documents to Markdown for LLM Ingestion — 3 Practical Scripts</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Tue, 09 Jun 2026 06:31:45 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/how-i-batch-convert-100-documents-to-markdown-for-llm-ingestion-3-practical-scripts-1jka</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/how-i-batch-convert-100-documents-to-markdown-for-llm-ingestion-3-practical-scripts-1jka</guid>
      <description>&lt;h1&gt;
  
  
  How I Batch-Convert 100+ Documents to Markdown for LLM Ingestion — 3 Practical Scripts
&lt;/h1&gt;

&lt;p&gt;I had 300 PDFs, 50 DOCX files, and a pile of PPTX decks sitting in a directory — all the internal docs from three years of client projects. I needed clean Markdown for my LLM pipeline, and "open one by one and copy-paste" wasn't going to cut it.&lt;/p&gt;

&lt;p&gt;Here's how I got it done in an afternoon with MarkItDown and three scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Markdown Matters for LLMs
&lt;/h2&gt;

&lt;p&gt;Before showing the code, let's talk about why this matters. LLMs charge by the token. Here's what that looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HTML — 23 tokens just for the boilerplate
&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1 class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Introduction&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Markdown — 3 tokens for the same heading
&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;# Introduction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a &lt;strong&gt;7.6x efficiency gap&lt;/strong&gt; on every heading, every paragraph wrapper, every table cell. When you're processing hundreds of documents into an LLM context window, the difference between raw HTML and clean Markdown can mean 3–8x fewer tokens. That translates directly to lower API costs, faster inference, and more documents fitting into a single context window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://markitdown-pro.com" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt; is Microsoft's open-source document-to-Markdown converter — 140K+ GitHub stars, MIT licensed, and gaining ~200 stars a day. It handles PDF, DOCX, PPTX, Excel, and 10+ other formats, all converting to clean, consistent Markdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script 1: batch_convert.py — Recursive Directory Converter
&lt;/h2&gt;

&lt;p&gt;This is the workhorse. Point it at a directory, and it recursively finds every supported file, converts it, and drops the &lt;code&gt;.md&lt;/code&gt; next to the original.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Batch convert documents to Markdown using MarkItDown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;

&lt;span class="n"&gt;SUPPORTED_EXTENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.docx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pptx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.xlsx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.htm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.xml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.zip&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.msg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.eml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.rtf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.odt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.ods&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.odp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.epub&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;collect_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recursively collect all supported files.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_EXTENSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Convert a single file to Markdown. Returns True on success.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  ✗ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Batch convert documents to Markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;directory&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Root directory to scan recursively&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--output-dir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Output directory (mirrors source structure)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--dry-run&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;List files without converting&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is not a directory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No supported files found in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; file(s) to convert:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;input_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Converting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;convert_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Done: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; converted successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage is dead simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;markitdown
python batch_convert.py ./client-docs &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ./markdown-output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at a directory, it recursively finds every PDF, DOCX, PPTX, Excel, HTML, CSV, JSON, XML, email, RTF, ODF, and EPUB file, converts each one, and mirrors the directory structure in the output folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script 2: server.py — When You Need an API Instead of CLI
&lt;/h2&gt;

&lt;p&gt;Sometimes you're building a pipeline and need to trigger conversions via HTTP — from an n8n workflow, a Zapier webhook, or your own frontend. Here's a FastAPI wrapper that exposes MarkItDown as a REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;FastAPI server wrapping MarkItDown for API-based conversion.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PlainTextResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MarkItDown API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;SUPPORTED_CONTENT_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/pdf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/vnd.openxmlformats-officedocument.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wordprocessingml.document&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/vnd.openxmlformats-officedocument.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;presentationml.presentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/vnd.openxmlformats-officedocument.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spreadsheetml.sheet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/xml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/zip&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message/rfc822&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/rtf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/epub+zip&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/vnd.oasis.opendocument.text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/convert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PlainTextResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(...)):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Upload a document, get back Markdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_CONTENT_TYPES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;415&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.tmp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyfileobj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_content&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUPPORTED_CONTENT_TYPES&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any service in your stack can POST a file and get Markdown back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/convert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@report.docx"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Script 3: pdf_cleanup.py — Post-Processing for Messy PDFs
&lt;/h2&gt;

&lt;p&gt;PDF conversion is where things get ugly. Scanned documents come out as empty strings, multi-column layouts produce jumbled text, and watermarks get scattered through the output. This script cleans up the most common artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Post-process MarkItDown output for cleaner LLM-ready text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Apply a pipeline of cleanup operations.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Collapse 3+ blank lines into 2
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\n{3,}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Strip trailing whitespace on each line
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[ \t]+$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove lines that are entirely non-alphanumeric (watermarks, separators)
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^\s*[^a-zA-Z0-9\u4e00-\u9fff]{3,}\s*$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Fix broken hyphenation: "con-\nvert" → "convert"
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(\w)-\n(\w)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1\2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Merge single-line-break paragraphs that aren't actually separate
&lt;/span&gt;    &lt;span class="c1"&gt;# (keeps intentional blank-line paragraph separators)
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;([^\n])\n([^\n#\-\*\d\s])&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\1 \2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Normalize Unicode quotes and dashes
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u2018&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u2019&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u201c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u201d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u2013&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u2014&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Clean up MarkItDown output for LLM use&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Markdown files to clean&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--in-place&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Modify files in place&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--output-dir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Write cleaned files to a separate directory&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipping non-markdown file: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=== &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;savings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;savings&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: removed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;savings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chars (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;savings&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it after batch_convert to clean up all your output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python pdf_cleanup.py ./markdown-output/&lt;span class="k"&gt;*&lt;/span&gt;.md &lt;span class="nt"&gt;--in-place&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Option: One Command, Zero Setup
&lt;/h2&gt;

&lt;p&gt;If you don't want to deal with Python environments (looking at you, PDF dependencies), everything's containerized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Jakeshadow/markitdown-batch-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;markitdown-batch-examples

&lt;span class="c"&gt;# Mount your documents and run&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; /path/to/docs:/input converter &lt;span class="se"&gt;\&lt;/span&gt;
  python batch_convert.py /input &lt;span class="nt"&gt;--output-dir&lt;/span&gt; /input/markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Docker image includes all the heavy dependencies (pdfminer, python-docx, openpyxl) so you don't fight with system libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  MarkItDown vs Unstructured.io: When to Use Which
&lt;/h2&gt;

&lt;p&gt;I evaluated both before committing to MarkItDown. Here's the quick breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown&lt;/strong&gt;: MIT license, Python-native, dead simple API (&lt;code&gt;md.convert("file.pdf")&lt;/code&gt;), produces clean semantic Markdown. Best for batch conversion pipelines where you want LLM-ready output with zero configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured.io&lt;/strong&gt;: Apache 2.0, supports more esoteric formats (JPG OCR, EML parsing with metadata), but heavier dependencies and more complex setup. Better if you need structured chunking metadata alongside the text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my use case — batch converting internal docs for LLM pipelines — MarkItDown won on simplicity and output quality. No configuration files, no partitioning strategies to configure. Just point it at a file and get Markdown.&lt;/p&gt;

&lt;p&gt;Full comparison with code examples on both sides: &lt;a href="https://markitdown-pro.com/projects/markitdown/vs-unstructured/" rel="noopener noreferrer"&gt;MarkItDown vs Unstructured.io&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Three scripts, one afternoon, and 300+ documents went from a mess of proprietary formats to clean, token-efficient Markdown. The whole pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;batch_convert.py&lt;/code&gt; — mass conversion of everything in a directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pdf_cleanup.py&lt;/code&gt; — post-process to fix PDF artifacts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server.py&lt;/code&gt; — REST API when you need programmatic access&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full guide with installation walkthroughs, Docker setup, and performance benchmarks is at &lt;a href="https://markitdown-pro.com" rel="noopener noreferrer"&gt;markitdown-pro.com&lt;/a&gt;. All three scripts plus Docker Compose files are in the &lt;a href="https://github.com/Jakeshadow/markitdown-batch-examples" rel="noopener noreferrer"&gt;GitHub companion repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stop opening files one by one. Let the scripts do it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>datascience</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deploying Hermes WebUI with Docker — The 8 Errors I Hit and How to Fix Them</title>
      <dc:creator>Vigoss Luke</dc:creator>
      <pubDate>Tue, 09 Jun 2026 06:28:42 +0000</pubDate>
      <link>https://dev.to/vigoss_luke_3604c1d0e9b4a/deploying-hermes-webui-with-docker-the-8-errors-i-hit-and-how-to-fix-them-4iip</link>
      <guid>https://dev.to/vigoss_luke_3604c1d0e9b4a/deploying-hermes-webui-with-docker-the-8-errors-i-hit-and-how-to-fix-them-4iip</guid>
      <description>&lt;h1&gt;
  
  
  Deploying Hermes WebUI with Docker — The 8 Errors I Hit and How to Fix Them
&lt;/h1&gt;

&lt;p&gt;I needed a web UI for the Hermes Agent. Docker Compose looked simple. Eight errors later, I had a working deployment — and a collection of scars I'm about to save you from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Hermes WebUI?
&lt;/h2&gt;

&lt;p&gt;Hermes WebUI is a self-hosted web interface for the &lt;a href="https://hermesdocker.com" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt; — an open-source AI agent framework with 140K+ GitHub stars and 2.9 trillion daily tokens served through OpenRouter. It's the browser-based dashboard you need to chat with your agent, manage conversations, and monitor usage, all from a Docker container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Minute Happy Path
&lt;/h2&gt;

&lt;p&gt;Before we dive into the errors, here's what &lt;em&gt;should&lt;/em&gt; happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Jakeshadow/hermes-webui-docker-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hermes-webui-docker-examples/single-container
&lt;span class="c"&gt;# Create a .env file with your API keys (see .env.example)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;, and you're chatting with your Hermes Agent. This actually works for most people on Linux. But if you're on macOS, RHEL, or doing anything slightly custom — buckle up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error #1: UID/GID Mismatch (The Silent Killer)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Container starts fine. You mount a volume for persistent data. The container can't write to it — permission denied, or worse, it writes but you can't see the files on your host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The Docker container runs as user UID 1000 (standard Linux user). macOS users run as UID 501. Linux users might be UID 1000, but their GID could be anything. When the container writes files with UID 1000, your host user can't read them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix — set WANTED_UID and WANTED_GID in your &lt;code&gt;.env&lt;/code&gt; file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
WANTED_UID=501
WANTED_GID=20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, check your UID/GID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;   &lt;span class="c"&gt;# usually 501&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;   &lt;span class="c"&gt;# usually 20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;   &lt;span class="c"&gt;# usually 1000&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;   &lt;span class="c"&gt;# usually 1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entrypoint script picks up these variables and creates a matching user inside the container. No more permission whack-a-mole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error #2: Empty Mounted Directories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; You mount &lt;code&gt;./data:/app/data&lt;/code&gt;, the container starts, but &lt;code&gt;./data&lt;/code&gt; on your host stays empty even though the app seems to be running fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; Docker volume mounts work in one direction at container creation. If you mount an empty host directory to a non-empty container path, the empty host directory &lt;em&gt;wins&lt;/em&gt; — it overlays and hides whatever was in the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Pre-create the expected directory structure on the host first, or let the entrypoint script handle initialization &lt;em&gt;after&lt;/em&gt; the mount. With Hermes WebUI, the companion repo includes a &lt;code&gt;scripts/init-data.sh&lt;/code&gt; that handles this. Run it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/init-data.sh
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a deep dive on Docker volume mounting behavior, check out the &lt;a href="https://hermesdocker.com/docker-volume-mounting/" rel="noopener noreferrer"&gt;detail page on hermesdocker.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error #3: &lt;code&gt;docker_mount_cwd_to_workspace&lt;/code&gt; Permission Errors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; You're using the three-container production setup, and the workspace mount throws errors like &lt;code&gt;Permission denied: /workspace&lt;/code&gt;. The gateway container can't access files that clearly exist on the host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; This one is a double-whammy. First, the UID/GID issue from Error #1 applies here too. Second, the Docker socket proxy doesn't inherit the right file permissions when the mount crosses container boundaries in the three-container setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Make sure &lt;code&gt;.env&lt;/code&gt; has matching UID/GID across &lt;em&gt;all&lt;/em&gt; containers, not just the web UI. In the three-container setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Applies to webui, gateway, AND hermes-agent containers
WANTED_UID=1000
WANTED_GID=1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rebuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Error #4: SELinux on RHEL/Fedora
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Everything looks right, but the container can't access &lt;em&gt;any&lt;/em&gt; mounted volumes. Logs show &lt;code&gt;avc: denied&lt;/code&gt; messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; SELinux labels on the host directory block container access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix — add &lt;code&gt;:Z&lt;/code&gt; to your volume mounts in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data:Z&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./workspace:/workspace:Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:Z&lt;/code&gt; flag tells Docker to relabel the directory for container access. Use &lt;code&gt;:z&lt;/code&gt; if multiple containers share the same volume, &lt;code&gt;:Z&lt;/code&gt; if it's exclusive to one container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose This Over Open WebUI
&lt;/h2&gt;

&lt;p&gt;I compared Hermes WebUI with Open WebUI before committing. The short version: Open WebUI is feature-rich but heavy — it bundles RAG, document parsing, and a web search engine into one container. Hermes WebUI is focused: it does the agent chat interface and does it well, with three deployment modes that scale from a single Docker command to a production-grade multi-container setup.&lt;/p&gt;

&lt;p&gt;The three modes matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-container&lt;/strong&gt;: One &lt;code&gt;docker compose up -d&lt;/code&gt;, perfect for testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-container&lt;/strong&gt;: Separates the web UI from the gateway for cleaner isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three-container&lt;/strong&gt;: Full production setup with isolated gateway, agent runtime, and web UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the full comparison breakdown, there's a &lt;a href="https://hermesdocker.com/vs-open-webui/" rel="noopener noreferrer"&gt;dedicated page on Hermes WebUI vs Open WebUI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Docker makes deployment look deceptively simple — until you hit a permission error at 11 PM. The eight errors I hit all boil down to three root causes: user ID mismatches between host and container, volume mount directionality, and SELinux on enterprise distros. Fix those three and you're golden.&lt;/p&gt;

&lt;p&gt;All the working Docker Compose files (single, dual, and three-container modes) are in the &lt;a href="https://github.com/Jakeshadow/hermes-webui-docker-examples" rel="noopener noreferrer"&gt;GitHub companion repo&lt;/a&gt;. Clone it, set your &lt;code&gt;.env&lt;/code&gt;, and skip the debugging I already did for you.&lt;/p&gt;

&lt;p&gt;Happy deploying.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
