pyssg is pre-1.0 and under active development - APIs, config, and themes may change.
pyssg.contrib.obsidian

3 min read

pyssg.contrib.obsidian

Contrib: Obsidian vault integration.

Turns an Obsidian vault into a PySSG site with one call. It is a thin composition layer -- it owns no new build algorithm beyond attachment handling; everything else is the existing built-in pipeline wired with vault-friendly defaults:

  • vault noise (.obsidian/, .trash/, .git/) is excluded from the content walk via :func:pyssg.plugins.directory_loader's exclude filter; the Obsidian adapter discovers folder-specific excludes (Templates, daily notes) from the vault's own settings and passes them in too;
  • selective publishing is delegated to the :mod:pyssg.contrib.publish_gate plugin (denylist by default for a whole-vault wiki: a note is published unless it sets publish: false; switch to an allowlist with publish_required);
  • Hugo-style _index.md section pages are routed to their directory root;
  • wikilinks /
    missing: note
    embeds / #tags are handled by the built-in wikilink / transclude / taxonomy plugins.

The one genuinely Obsidian-specific algorithm lives here: attachment embeds. Obsidian writes image/file embeds as

missing: image.png
and stores the binary anywhere in the vault. :class:ObsidianAttachmentsPlugin resolves such embeds by filename, rewrites them to <img>/<a> pointing at a stable URL, and copies the referenced binaries into the output -- the built-in pipeline only copies a theme's assets, not vault attachments.

Everything is pure: the attachment index and the copy are deterministic functions of the on-disk vault, so builds stay byte-identical and incremental rebuilds equal full rebuilds.

rewrite_attachment_embeds(build: Build, html: str, exclude: tuple[str, ...]) -> str

finalize_content tap: resolve

missing: file.ext
attachment embeds.

Only embeds whose target carries a non-.md extension are handled here; note embeds (

missing: Note
) are left untouched for the transclude plugin. An attachment that cannot be resolved renders as a broken-embed marker rather than vanishing.

class ObsidianAttachmentsPlugin

Resolves

missing: file.ext
embeds and copies vault attachments to output.

ObsidianAttachmentsPlugin.__init__(self, *, exclude: Sequence[str] | None = None) -> None

ObsidianAttachmentsPlugin.apply(self, builder: Builder) -> None

obsidian_attachments(*, exclude: Sequence[str] | None = None) -> ObsidianAttachmentsPlugin

Factory for the attachment plugin (used standalone or by :func:obsidian_plugins).

section_index_url(url: str, source_path: str | None) -> str

Map a Hugo-style _index.md page onto its directory root URL.

content/guide/_index.md is routed to /guide/ instead of /guide/_index/ (and a top-level _index.md to /), so a vault using the _index section-page convention gets real section landing pages without an explicit permalink. Any other page is returned unchanged. Pure: the result depends only on the URL and source path.

class ObsidianSectionIndexPlugin

Routes _index.md files to their directory root (Hugo section pages).

ObsidianSectionIndexPlugin.apply(self, builder: Builder) -> None

obsidian_section_index() -> ObsidianSectionIndexPlugin

Factory for the _index.md section-page router.

obsidian_plugins(*, include: Sequence[str] | None = None, exclude: Sequence[str] | None = None, publish_required: bool = False, publish_key: str = 'publish', section_index: bool = True, highlight_style: str = 'default', rss_title: str | None = None) -> list[Plugin]

Return the full Obsidian-flavored plugin pipeline, in apply order.

The default vault excludes (:data:DEFAULT_VAULT_EXCLUDE) are always applied; exclude adds to them and include (if given) restricts which files load. publish_required selects the publishing mode: the default False is a denylist (every note is published unless it sets publish: false), suited to a whole-vault wiki; pass True for an allowlist (publish only publish: true notes). section_index (default on) routes Hugo-style _index.md files to their directory root. Drop the result straight into a :class:~pyssg.Config (Config(plugins=obsidian_plugins())) or splice extra plugins after it.