A CLI tool for publishing notes from an Obsidian vault to a Hugo blog. It handles frontmatter transformation, Obsidian wikilink conversion, and media embedding.
This section provides end-to-end instructions for publishing a blog post from Obsidian to a live Cloudflare Pages site.
Create a new markdown file in your Obsidian vault (default: ~/Notes/Blog/). Add YAML frontmatter at the top:
---
title: My Amazing Blog Post
date: 2026-01-15
publish: true
draft: false
tags:
- tutorial
- python
author: Your Name
description: A brief summary of your post for SEO and previews.
content_type: post
categories:
- programming
---
# My Amazing Blog Post
Your content here...
| Field | Description |
|---|---|
title |
Post title (or extracted from first H1 heading) |
date |
Publication date in YYYY-MM-DD format |
publish |
Must be true to be discovered by the pipeline |
tags |
List of tags for categorization |
| Field | Description |
|---|---|
draft |
Set to true to mark as draft (won’t appear in production) |
author |
Author name |
description |
SEO description/summary |
content_type |
Routing: post (default), photography, or project |
categories |
List of categories |
series |
Series name for multi-part posts |
| Content Type | Additional Fields |
|---|---|
post |
author, categories, series |
photography |
location, camera, lens |
project |
github, technologies, status |
Embed media using Obsidian’s syntax:
## Adding Images
![[2026/01/my-photo.jpg]]
## Adding Videos
![[tutorials/demo.mp4]]
## Adding Audio
![[podcasts/episode-01.mp3]]
Media files should be in your Media folder (default: ~/Notes/Media/). Paths are relative to that folder.
For photography posts or posts with image galleries, add a ## Pictures section:
## Pictures
![[2026/01/gallery/image1.jpg]]
![[2026/01/gallery/image2.png]]
![[2026/01/gallery/image3.webp]]
This section is automatically converted to a nanogallery2 photo gallery in the final Hugo output.
Use Obsidian wikilinks to reference other posts:
Check out my [[Previous Post]] for more context.
You might also like [[Another Article|this related article]].
For related content, add an ## Associations section:
## Associations
- [[Getting Started Guide]]
- [[Advanced Tips]]
- [[FAQ]]
This section is converted to Hugo internal links and renamed to “## Related” (or can be removed with flags).
Before publishing, validate your post to catch issues:
cd scripts/
python publish.py validate ~/Notes/Blog/my-post.md
This checks:
Run a dry-run to see the converted output without making changes:
python publish.py publish ~/Notes/Blog/my-post.md --dry-run
This shows:
When ready, publish the post:
# With media upload to MinIO
python publish.py publish ~/Notes/Blog/my-post.md
# Skip media upload (use s3cdn shortcode placeholders)
python publish.py publish ~/Notes/Blog/my-post.md --skip-upload
# Auto-confirm without prompts
python publish.py publish ~/Notes/Blog/my-post.md --yes
The command will:
After publishing, build the Hugo site to verify everything works:
# From the blog root directory
hugo --environment production
# Or use the Makefile
make build
Check for any errors or warnings in the build output.
To preview your post before deploying:
# Start the Hugo development server
make dev
# Or directly
hugo server -e development --buildDrafts --buildFuture
Visit http://localhost:1313 to see your site.
Commit and push your changes to trigger a Cloudflare Pages deployment:
git add content/
git commit -m "Add new blog post: My Amazing Blog Post"
git push origin develop
Then create a pull request to merge develop into master. Cloudflare Pages will automatically build and deploy when changes are merged to master.
# 1. Validate
python scripts/publish.py validate ~/Notes/Blog/my-post.md
# 2. Preview (dry-run)
python scripts/publish.py publish ~/Notes/Blog/my-post.md --dry-run
# 3. Publish
python scripts/publish.py publish ~/Notes/Blog/my-post.md
# 4. Build Hugo
hugo --environment production
# 5. Commit and push
git add content/
git commit -m "Add: My Amazing Blog Post"
git push origin develop
The repository includes a Makefile with common commands:
| Command | Description |
|---|---|
make dev |
Start Hugo development server |
make build |
Build site for production |
make publish-scan |
Scan vault for publishable notes |
make publish-list |
Show target paths for all notes |
make publish-dry |
Dry-run publish all notes |
make test-publish |
Dry-run + Hugo build verification |
make test |
Run all tests |
make clean |
Remove generated files |
To publish all notes at once:
# Preview all publishable notes
python scripts/publish.py publish --all --dry-run
# Publish all notes
python scripts/publish.py publish --all --yes
scanpublish: true is in the frontmatter~/Notes/Blog/ by default)--- markers)scripts/.envpython publish.py media <path> to check references[[Page Name]] syntax[[Page Name|Display Text]]## Pictures section (not ### Pictures)![[path]] embed syntax--no-gallery to disable if unwantedcd scripts/
pip install -r requirements.txt
The tool can automatically upload media files (images, videos, audio) to MinIO or any S3-compatible storage. This is optional—without MinIO configured, media embeds will use the `` Hugo shortcode.
Copy the example environment file and fill in your credentials:
cp scripts/.env.example scripts/.env
Edit scripts/.env with your MinIO configuration:
# MinIO Server Endpoint (without protocol)
MINIO_ENDPOINT=minio.example.com:9000
# Credentials
MINIO_ACCESS_KEY=your-access-key-here
MINIO_SECRET_KEY=your-secret-key-here
# Bucket name (will be created if it doesn't exist)
MINIO_BUCKET=blog-assets
# Use HTTPS (true/false)
MINIO_SECURE=true
The tool works with any S3-compatible storage:
| Provider | Example Endpoint |
|---|---|
| Self-hosted MinIO | minio.example.com:9000 |
| AWS S3 | s3.amazonaws.com |
| DigitalOcean Spaces | nyc3.digitaloceanspaces.com |
| Backblaze B2 | s3.us-west-002.backblazeb2.com |
Media files are uploaded preserving their path structure:
Local: /home/user/Media/2021/06/photo.jpg
MinIO: bucket/assets/2021/06/photo.jpg
URL: https://minio.example.com/bucket/assets/2021/06/photo.jpg
# Scan your vault for publishable notes
python scripts/publish.py scan
# See where notes will be published
python scripts/publish.py list
# Preview media files that would be uploaded
python scripts/publish.py media ~/Notes/Blog/my-post.md
# Convert a specific note (preview first)
python scripts/publish.py convert ~/Notes/Blog/my-post.md --dry-run
# Convert and write the file (with media upload)
python scripts/publish.py convert ~/Notes/Blog/my-post.md
# Convert without uploading media (test mode)
python scripts/publish.py convert ~/Notes/Blog/my-post.md --skip-upload
scan - Find Publishable NotesScans your Obsidian vault for markdown files with publish: true in their frontmatter.
python scripts/publish.py scan
# Use a custom vault location
python scripts/publish.py scan --vault ~/Documents/MyVault
Output example:
Filename Title Date Tags
-----------------------------------------------------------------------------------------------
my-first-post.md My First Blog Post 2024-01-15 python, tutorial
another-article.md Another Article 2024-02-20 hugo, blogging
Found 2 publishable note(s).
list - Preview Target PathsShows all publishable notes along with their target Hugo output paths.
python scripts/publish.py list
# Custom vault and output directory
python scripts/publish.py list --vault ~/Notes --output content/english/blog
Output example:
Filename Title Date Target Path
---------------------------------------------------------------------------------------------------------
my-first-post.md My First Blog Post 2024-01-15 content/english/post/2024-01-15-my-first-blog-post.md
Found 1 publishable note(s).
media - Preview Media ReferencesLists all media files (images, videos, audio) referenced in a note without uploading them. Useful for checking what would be uploaded before running convert.
python scripts/publish.py media ~/Notes/Blog/my-post.md
Output example:
Media references in: my-post.md
============================================================
Resolved (3 file(s)):
2021/06/photo.jpg
-> /home/user/Media/2021/06/photo.jpg
2021/06/video.mp4
-> /home/user/Media/2021/06/video.mp4
2021/06/diagram.png
-> /home/user/Media/2021/06/diagram.png
Missing (1 file(s)):
2021/06/deleted-image.jpg [NOT FOUND]
Summary: 4 reference(s), 3 resolved, 1 missing
convert - Convert a NoteConverts a single Obsidian note to Hugo format. When MinIO is configured, media files are automatically uploaded and their URLs are embedded in the converted output.
# Preview conversion without writing
python scripts/publish.py convert ~/Notes/Blog/my-post.md --dry-run
# Convert and write to Hugo content directory (uploads media to MinIO)
python scripts/publish.py convert ~/Notes/Blog/my-post.md
# Specify custom output directory
python scripts/publish.py convert ~/Notes/Blog/my-post.md --output content/english/blog
# Skip media upload (useful for testing or when MinIO isn't configured)
python scripts/publish.py convert ~/Notes/Blog/my-post.md --skip-upload
Options:
--dry-run: Preview the conversion without writing files or uploading media--output DIR: Custom Hugo output directory (default: content/english/post)--skip-upload: Skip uploading media to MinIO (media embeds will use `` shortcode)With --dry-run:
[DRY RUN] Would convert: /home/user/Notes/Blog/my-post.md
[DRY RUN] Target path: content/english/post/2024-01-15-my-post.md
[DRY RUN] Media files found: 3
[DRY RUN] Media files missing: 1
--- Preview of converted content ---
---
title: "My Post"
date: 2024-01-15
draft: false
tags:
- python
- tutorial
---
This is my blog post content...
Media Upload Behavior:
rich is installedNotes must have publish: true in their YAML frontmatter to be discovered:
---
title: My Blog Post
date: 2024-01-15
publish: true
tags:
- python
- tutorial
---
The tool automatically converts Obsidian-specific syntax:
| Obsidian Syntax | Hugo Output |
|---|---|
[[Page Name]] |
[Page Name](/post/page-name/) |
[[Page Name\|Display Text]] |
[Display Text](/post/page-name/) |
![[image.jpg]] |
 |
![[video.mp4]] |
<video controls><source src="/video.mp4" type="video/mp4"></video> |
![[audio.mp3]] |
<audio controls><source src="/audio.mp3" type="audio/mpeg"></audio> |
The tool normalizes Obsidian frontmatter to Hugo-compatible format:
title, date, draft, tagsYYYY-MM-DD formatpublish field (Obsidian-specific)author, description, categories, series, etc.Output filenames are generated in Hugo’s standard format:
YYYY-MM-DD-slug-from-title.md
Example: A post titled “My First Blog Post” dated 2024-01-15 becomes:
2024-01-15-my-first-blog-post.md
| Module | Purpose |
|---|---|
publish.py |
Main CLI entry point |
obsidian_parser.py |
Parse Obsidian notes and extract frontmatter |
syntax_converter.py |
Convert wikilinks, image embeds, and media embeds |
frontmatter_transformer.py |
Transform Obsidian frontmatter to Hugo format |
hugo_writer.py |
Write formatted Hugo markdown files |
media_extractor.py |
Extract and resolve media references from Obsidian syntax |
minio_uploader.py |
Upload media files to MinIO/S3-compatible storage |
cd scripts/
python -m pytest -v
# Run tests for a specific module
python -m pytest test_obsidian_parser.py -v
python -m pytest test_syntax_converter.py -v
python -m pytest test_frontmatter_transformer.py -v
python -m pytest test_hugo_writer.py -v
python -m pytest test_publish_cli.py -v
python -m pytest test_media_extractor.py -v
python -m pytest test_minio_uploader.py -v
| Setting | Default Value |
|---|---|
| Obsidian vault | ~/Notes |
| Hugo output directory | content/english/post |
| Media folder | ~/Notes/Media |
| MinIO asset prefix | assets |
| Skipped directories | People |