Upload markdown local images to S3/R2 storage with auto watermark and link replacement.
- Scan markdown files for local image references (
and<img src="path">) - Upload images to any S3-compatible storage (Cloudflare R2, AWS S3, MinIO, etc.)
- Auto watermark with configurable opacity, size, position
- MD5 hash-based deduplication across files
- Replace local paths with public URLs in markdown
- CLI and programmatic API
npm install @maigic/blogtoolnpx blogtool initThis generates blogtool.config.mjs in the current directory with all available options and comments.
Edit blogtool.config.mjs to fill in your S3/R2 credentials, or set environment variables:
export R2_ACCOUNT_ID=your_account_id
export R2_ACCESS_KEY_ID=your_access_key
export R2_SECRET_ACCESS_KEY=your_secret_key
export R2_BUCKET=your_bucket# Preview what will be processed
npx blogtool --dry-run
# Upload images and replace links
npx blogtool
# Process a single file
npx blogtool -f src/content/blog/my-post.md
# Use a custom config file
npx blogtool -c ./custom.config.mjsUsage: blogtool [options] [command]
Upload markdown local images to S3/R2 with watermark and link replacement
Commands:
init [options] generate a config file template
Options:
-c, --config <path> config file path (default: "blogtool.config.mjs")
--dry-run preview without uploading
-f, --file <path> process a single markdown file
-V, --version output the version number
-h, --help display help for command
Usage: blogtool init [options]
generate a config file template
Options:
-o, --output <path> output file path (default: "blogtool.config.mjs")
-h, --help display help for command
import { processImages } from "@maigic/blogtool";
const result = await processImages({
storage: {
endpoint: "https://xxx.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "...",
secretAccessKey: "...",
},
bucket: "my-bucket",
publicDomain: "img.example.com",
},
watermark: {
imagePath: "./logo.svg",
},
markdown: {
dirs: ["./src/content/blog"],
},
dryRun: false,
});
console.log(`Uploaded: ${result.uploaded}, Skipped: ${result.skipped}`);| Option | Type | Required | Default | Description |
|---|---|---|---|---|
endpoint |
string |
Yes | - | S3-compatible endpoint URL |
region |
string |
No | "auto" |
Region |
credentials.accessKeyId |
string |
Yes | - | Access key ID |
credentials.secretAccessKey |
string |
Yes | - | Secret access key |
bucket |
string |
Yes | - | Bucket name |
publicDomain |
string |
Yes | - | Public access domain |
keyPrefix |
string |
No | "blog" |
Key prefix in bucket |
Uploaded files are stored as {keyPrefix}/{hash[0:2]}/{hash}{ext}, e.g. blog/4e/4ea7a...png.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
imagePath |
string |
Yes | - | Path to watermark image (SVG or PNG) |
opacity |
number |
No | 0.15 |
Opacity (0-1) |
maxSize |
number |
No | 120 |
Max watermark size in px |
minImageSize |
number |
No | 400 |
Skip watermark if image short side is below this |
padding |
number |
No | 16 |
Padding from edge in px |
position |
string |
No | "bottom-left" |
bottom-left, bottom-right, top-left, top-right |
Watermark size scales dynamically: 8% of the image's short side, capped at maxSize.
For SVG watermarks, paths are extracted and rendered in white with the specified opacity. Background rects are removed automatically.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
dirs |
string[] |
Yes | - | Directories to scan for markdown files |
filePattern |
string |
No | "**/*.md" |
Glob pattern (currently supports recursive flag) |
- Scans markdown files in configured directories
- Extracts local image paths (relative or absolute, not http/data URIs)
- Reads each image and optionally applies watermark via Sharp
- Computes MD5 hash of the processed image buffer
- Checks if the file already exists in storage (HEAD request)
- Uploads if new, skips if exists
- Replaces local paths in markdown with public URLs
- Writes updated markdown back to disk
- Create an R2 bucket in Cloudflare dashboard
- Go to Manage R2 API Tokens and create a token with "Object Read & Write" permission
- Copy the endpoint, access key ID, and secret access key
- Optionally bind a custom domain to the bucket for public access
Yes, any S3-compatible storage is supported. Just set the appropriate endpoint and region.
MIT