CLI tool that downloads web articles as AI-friendly markdown with locally stored images.
Extracts clean article content from any URL using Jina Reader, downloads all images locally, rewrites markdown image references to relative paths, and packages everything into a self-contained folder ready for AI workflows.
Requires Python 3.10+.
pip install git+https://github.com/Benny-Lewis/oh-hi-markdown.gitFor local development:
git clone https://github.com/Benny-Lewis/oh-hi-markdown.git
cd oh-hi-markdown
pip install -e ".[dev]"Both ohmd and ohhimark commands are installed — they're identical.
# Download an article to the current directory
ohmd https://example.com/some-article
# Specify an output directory
ohmd https://example.com/some-article -o ~/articles
# Overwrite if the output folder already exists
ohmd https://example.com/some-article --force
# Both commands are identical
ohhimark https://example.com/some-article./how-nuclear-batteries-work-a-deep-dive/
├── article.md # Article content with local image references
├── images/ # Downloaded images (only if images were found)
│ ├── 001-reactor-diagram.png
│ ├── 002-power-output-chart.jpg
│ └── 003-comparison-table.png
└── ohmd.log # Detailed log of the download operation
The folder name is derived from the article title (slugified to lowercase with hyphens). article.md includes YAML front matter with metadata:
---
title: "How Nuclear Batteries Work: A Deep Dive"
author: "Jane Smith"
date: "2026-03-10"
source_url: "https://example.com/how-nuclear-batteries-work-a-deep-dive"
description: "An exploration of radioisotope thermoelectric generators."
downloaded: "2026-03-13T14:30:00Z"
tool: "ohmd v0.1.0"
---| Variable | Required | Description |
|---|---|---|
JINA_API_KEY |
No | Jina Reader API key for higher rate limits and AI-generated image alt text. Free keys available at jina.ai (no credit card required). |
export JINA_API_KEY=jina_your_key_here
ohmd https://example.com/some-articleWithout an API key, Jina imposes lower rate limits. If you hit HTTP 429 errors, setting a key will resolve it. When a key is set, the tool also requests AI-generated image descriptions via Jina's X-With-Generated-Alt feature.
| Code | Meaning |
|---|---|
0 |
Success or partial success — article saved (some image downloads may have failed). |
1 |
Invalid input — bad URL, missing arguments, or private/localhost IP. |
2 |
Article extraction failed — Jina error, rate limit, or empty response. |
3 |
Filesystem conflict or write failure — folder exists without --force, permissions error. |
4 |
Unexpected internal error. |
- Fetch — Sends the URL to Jina Reader (
r.jina.ai), which renders the page, strips navigation/ads/boilerplate, and returns clean markdown. IfJINA_API_KEYis set, requests AI-generated image alt text. - Parse — Extracts all
image references from the markdown. - Download — Fetches each image sequentially with a 30-second timeout and up to 3 retries (exponential backoff). Sets the
Refererheader to the article URL for hotlink-protected images. - Rewrite — Replaces remote image URLs with relative local paths (
./images/001-name.ext). Failed images keep their original remote URLs. - Publish — Writes all output to a temporary directory first, then atomically renames it to the final path. If
--forceis used and the folder exists, the old folder is backed up and only replaced after the new output is fully written.
- URLs are sent to Jina Reader. When you run
ohmd <url>, the URL is submitted to Jina's hosted API atr.jina.aifor content extraction. Jina processes the target page on their infrastructure. - Referer header on image requests. Image download requests include a
Refererheader set to the original article URL. This improves compatibility with hotlink-protected images but means the target image server sees which article URL triggered the download. - API key handling. If set,
JINA_API_KEYis sent as a Bearer token to Jina. It is never written to log files or console output. - No telemetry. The tool does not phone home or collect usage data beyond the Jina API call required for content extraction.
- Single URL per invocation. Use a shell loop for batch processing:
for url in $(cat urls.txt); do ohmd "$url"; done - No paywall or authentication support. Login-gated content is not supported.
- Rate limits without API key. Jina imposes lower rate limits without a
JINA_API_KEY. If you hit HTTP 429, get a free key. - Some sites return HTTP 451. Jina cannot access content restricted for legal reasons.
- Windows support is best-effort. Tested on macOS and Linux. Atomic rename behavior is not guaranteed on Windows.
- HTML
<img>tags are best-effort. The parser targets standard markdown image syntax (). HTML image tags in Jina's output may not be processed. - No image size limits enforced. Warnings are emitted for images over 10 MB, total downloads over 50 MB, or more than 50 images, but downloads are not blocked.
# Run tests (32 unit tests, ~0.7s)
python -m pytest --tb=short -q
# Lint
ruff check && ruff format --checkREQUIREMENTS.md— Full v1 specificationDESIGN.md— Technical design documentBACKLOG.md— Future plans and v2+ ideas