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
Metadata
Release files for obsidian-publisher 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| obsidian_publisher-0.1.0.tar.gz | 31.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| obsidian_publisher-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.2 kB
Release files / obsidian_publisher-0.1.0.tar.gz
| Download URL | obsidian_publisher-0.1.0.tar.gz |
|---|---|
| Size | 31.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1d06075227420cfd3c9574fbe029130b873f3bb67da08a38809b535ae9fa43f5
|
|
BLAKE2b-256 checksum How to use checksums |
c781a8fb16d3edbe039aeeab913f4b964a87755ca31240627ae6cdfe30d595d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / obsidian_publisher-0.1.0-py3-none-any.whl
| Download URL | obsidian_publisher-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
20d7ee022cf87d9d4bae92175f9e9058ee43c626bb336d4fa7119dbb68f08a2a
|
|
BLAKE2b-256 checksum How to use checksums |
fbbd8d433ca56bb8bbc6f9364fe5822a3e2848d10d7df51cdb7ee67f2c1c6fcc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Security for obsidian-publisher 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.