Automatically upload local images in Markdown files to a server and replace local paths with public URLs.
- 🔍 Automatically detects local images in Markdown files
- 📤 Uploads images to a configured server endpoint or Google Cloud Storage
- 🔄 Replaces local paths with public URLs
- ⚡ Supports concurrent uploads with progress display
- 🔐 Basic authentication support for API backend
- ☁️ Direct Google Cloud Storage integration
- 🎯 Configurable via JSON file or environment variables
pip install .Or install in development mode:
pip install -e .Basic usage:
omelet buildmarkdown ./readme.mdThis will:
- Scan the markdown file for local images (e.g.,
) - Upload each image to the configured server
- Replace local paths with the returned public URLs
- Update the original markdown file
Omelet can be configured via:
Create .omelet.json in your home directory or current working directory:
{
"backend_url": "https://your-backend-url.com/webhook",
"username": "your-username",
"password": "your-password",
"use_gcs": false,
"gcs_bucket": "your-bucket-name"
}export OMELET_USERNAME="your-username"
export OMELET_PASSWORD="your-password"
export OMELET_USE_GCS="true" # Set to "true" to use Google Cloud Storage
export OMELET_GCS_BUCKET="your-bucket-name"To use Google Cloud Storage instead of the API backend:
Omelet uses your existing gcloud CLI authentication. Make sure you're authenticated:
# Authenticate with gcloud CLI
gcloud auth application-default loginSet in configuration file:
{
"use_gcs": true,
"gcs_bucket": "your-bucket-name"
}Or use environment variable:
export OMELET_USE_GCS="true"
export OMELET_GCS_BUCKET="your-bucket-name"Run omelet on a markdown file:
omelet buildmarkdown ./readme.mdThe tool will upload images to: gs://your-bucket-name/public/blog/{folder}/{filename}
The upload endpoint should:
- Accept POST requests with
multipart/form-data - Expect fields:
data: The image file (binary)folder: The folder name (taken from the markdown file's parent directory)
- Support Basic Authentication (if configured)
- Return JSON response:
{ "public_url": "https://example.com/path/to/uploaded-image.png" }
Before:
# My Article

Some content here...
After:
# My Article

Some content here...
- PNG
- JPG/JPEG
- GIF
- SVG
- WebP
- BMP
- ICO
Ghost's HTML-to-lexical converter rewrites markup it recognises: every <figure>
becomes its own image card, which collapses a multi-image grid to the first image
and concatenates the captions. Wrapping a block in <!--kg-card-begin: html--> and
<!--kg-card-end: html--> is the documented way to hand Ghost that block untouched.
Whatever sits between those markers skips the bleach allowlist too, so inline SVG,
style attributes and anything else survive to the published post. Script tags,
on* event handlers and javascript: URLs still fail the build. An <svg> written
outside the markers is an error rather than a silent strip, because the allowlist
has no SVG vocabulary and would leave only the text nodes behind.
<Diagram> writes the markers for you:
<Diagram src="./diagrams/01-architecture.svg" caption="Hình 1. Ba khối `KDA` rồi một khối **MLA**." />
<Diagram caption="Inline works too">
<svg viewBox="0 0 200 40" role="img" aria-label="...">...</svg>
</Diagram>srcinlines an.svgor.htmlfile at compile time, resolved next to the post.- Children are raw markup, never parsed as markdown.
captionis inline markdown. classappends to the emittedomelet-diagramclass.<img src="./local.png">inside the block is uploaded and rewritten like any other local image, so screenshots can carry a real<figcaption>.
<Widget> is the one slot in a post that is allowed to carry a <script>. It reads
an .html file sitting next to the post, drops the file in verbatim, and marks the
block with <!--omelet:widget--> right after <!--kg-card-begin: html-->. Sanitize
recognises that sentinel and skips the executable-markup check for that block only.
<Widget src="./widgets/bpe-demo.html" caption="Hình 3. Gõ một từ và xem `BPE` cắt nó." />srcis required, must end in.html, and is resolved inside the post's own directory. Anything else fails the build.captionis optional and parsed as inline markdown.classis optional and appends to the emittedomelet-widgetclass.- The tag must sit at the top level of the post. Nesting it inside another component
raises a
ComponentError, because a nested block never gets its own marker and its script would be stripped anyway. - omelet-cli ships no CSS for
.omelet-widget. The widget file carries its own inline styles.
Only the compiler writes that sentinel, and only around a file read from the post's
directory. Every other kg-card block - including one you type by hand - still fails
the build with a SanitizeError the moment it holds a <script> tag, an on*
attribute, or a javascript: URL.
- Non-existent image files are reported but don't stop processing
- Failed uploads are logged with error messages
- Original paths are preserved if upload fails
- The tool continues processing remaining images even if some fail
Install development dependencies:
pip install -e ".[dev]"Run tests:
pytestNguyen Anh Binh
Email: socrat.nguyeannhbinh@gmail.com
Website: omelet.tech
MIT License - see LICENSE file for details