Skip to main content

Obsidian Publisher

License: MIT Python 3.10+ GitHub last commit GitHub repo size Code style: black

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

  1. Initialize a config file:
obsidian-publish init config.yaml
  1. Edit config.yaml with your vault and site paths:
vault_path: ~/Obsidian/MyVault
output_path: ~/Sites/my-blog
source_dir: Zettelkasten
required_tags:
  - status/evergreen
  1. 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)

Source distribution for obsidian-publisher 0.1.0
File Size Uploaded Provenance
obsidian_publisher-0.1.0.tar.gz 31.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for obsidian-publisher 0.1.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page