Obsidian Publisher
A modular Python library for publishing notes from an Obsidian vault to static site generators like Hugo.
Features
- Wikilink Conversion: Converts Obsidian
[[wikilinks]]to standard markdown links - Tag Filtering: Publish only notes with specific tags (e.g.,
status/evergreen) - Tag Transformation: Filter, rename, and reformat tags for your site
- Image Optimization: Converts images to WebP with PNG fallback
- Frontmatter Processing: Transform frontmatter for Hugo or other SSGs
- Orphan Cleanup: Automatically removes unused images
Installation
pip install obsidian-publisher
Or install from source:
git clone https://github.com/akcube/obsidian-publisher.git
cd obsidian-publisher
pip install -e .
Quick Start
- Initialize a config file:
obsidian-publish init config.yaml
- Edit
config.yamlwith your vault and site paths:
vault_path: ~/Obsidian/MyVault
output_path: ~/Sites/my-blog
source_dir: Zettelkasten
required_tags:
- status/evergreen
- Publish all eligible notes:
obsidian-publish republish -c config.yaml
CLI Commands
# Republish all eligible notes
obsidian-publish republish -c config.yaml
# Preview without making changes
obsidian-publish republish -c config.yaml --dry-run
# Publish a specific note
obsidian-publish add "My Note Title" -c config.yaml
# Remove a published note
obsidian-publish delete "My Note Title" -c config.yaml
# List all publishable notes
obsidian-publish list-notes -c config.yaml
Configuration
Basic Configuration
# Paths
vault_path: ~/Obsidian/MyVault
output_path: ~/Sites/my-blog
source_dir: Zettelkasten # Subdirectory within vault
# Output directories
content_dir: content/posts
image_dir: static/images
# Image sources within vault
image_sources:
- assets
- attachments
Tag Filtering
# Required tags (note must have at least one)
required_tags:
- status/evergreen
# Excluded tags (note with any of these is skipped)
excluded_tags:
- status/draft
- status/private
Link Transforms
# Relative links: [Title](slug.md)
link_transform:
type: relative
# Absolute links: [Title](/blog/slug)
link_transform:
type: absolute
prefix: /blog
# Hugo ref shortcode: [Title]({{< ref "slug" >}})
link_transform:
type: hugo_ref
Tag Transforms
# Filter tags by prefix and change separator
tag_transform:
prefixes:
- domain
- type
replace_separator:
- "/"
- "-"
# Result: domain/cs/algo → domain-cs-algo
Frontmatter Settings
frontmatter:
hugo: true
author: Your Name
Image Optimization
optimize_images: true
max_image_width: 1920
webp_quality: 85
image_path_prefix: /images
Python API
Basic Usage
from pathlib import Path
from obsidian_publisher.core.publisher import Publisher, PublisherConfig
config = PublisherConfig(
vault_path=Path("~/Obsidian/MyVault"),
output_path=Path("~/Sites/my-blog"),
source_dir="Zettelkasten",
required_tags=["status/evergreen"],
)
publisher = Publisher(config)
result = publisher.republish()
print(f"Published: {result.published}")
print(f"Failed: {result.failed}")
Custom Transforms
from obsidian_publisher.transforms.links import absolute_link
from obsidian_publisher.transforms.tags import filter_by_prefix, replace_separator, sort_tags, compose
from obsidian_publisher.transforms.frontmatter import hugo_frontmatter
# Create custom transforms
link_transform = absolute_link("/blog")
tag_transform = compose(
filter_by_prefix("domain", "type"),
replace_separator("/", "-"),
sort_tags(), # Sort tags alphabetically
)
frontmatter_transform = hugo_frontmatter("Author Name")
publisher = Publisher(
config,
link_transform=link_transform,
tag_transform=tag_transform,
frontmatter_transform=frontmatter_transform,
)
Architecture
The library is designed with modularity in mind:
obsidian_publisher/
├── core/
│ ├── discovery.py # VaultDiscovery - finds publishable notes
│ ├── processor.py # ContentProcessor - transforms content
│ ├── publisher.py # Publisher - orchestrates everything
│ └── models.py # Data models
├── transforms/
│ ├── links.py # Link transform factories
│ ├── tags.py # Tag transform factories
│ └── frontmatter.py # Frontmatter transform factories
├── images/
│ └── optimizer.py # ImageOptimizer - WebP conversion
└── cli/
└── main.py # Click CLI
Transform Pattern
All transforms are factory functions that return callables:
# Link transform: (title, slug) -> markdown_link
def absolute_link(prefix: str = "") -> LinkTransform:
def transform(title: str, slug: str) -> str:
return f"[{title}]({prefix}/{slug})"
return transform
# Tag transform: (tags) -> tags
def filter_by_prefix(*prefixes: str) -> TagTransform:
def transform(tags: List[str]) -> List[str]:
return [t for t in tags if any(t.startswith(p) for p in prefixes)]
return transform
def sort_tags() -> TagTransform:
return lambda tags: sorted(tags)
Development
# Clone and install dev dependencies
git clone https://github.com/akcube/obsidian-publisher.git
cd obsidian-publisher
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=obsidian_publisher
License
MIT License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
obsidian_publisher-0.1.0.tar.gz
(31.5 kB
view details)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file obsidian_publisher-0.1.0.tar.gz.
File metadata
- Download URL: obsidian_publisher-0.1.0.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d06075227420cfd3c9574fbe029130b873f3bb67da08a38809b535ae9fa43f5
|
|
| MD5 |
63ab5f361e68e62ef9f3f89063d49477
|
|
| BLAKE2b-256 |
c781a8fb16d3edbe039aeeab913f4b964a87755ca31240627ae6cdfe30d595d4
|
File details
Details for the file obsidian_publisher-0.1.0-py3-none-any.whl.
File metadata
- Download URL: obsidian_publisher-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20d7ee022cf87d9d4bae92175f9e9058ee43c626bb336d4fa7119dbb68f08a2a
|
|
| MD5 |
e1e582b62742daf7a71b5719adbc59d2
|
|
| BLAKE2b-256 |
fbbd8d433ca56bb8bbc6f9364fe5822a3e2848d10d7df51cdb7ee67f2c1c6fcc
|