Skip to main content Kainoa's 💩

Publishing Obsidian Notes as a Blog

Publishing Obsidian notes as a blog

My search to find a way to publish my Obsidian vault

Requirements

Utilise Obsidian as a CMS

  • That means writing articles/notes in obsidian markdown
  • Compatibility with WikiLinks and embeds
  • Compatibility with Markdown Links (obv) and embeds
  • Compatibility with Obsidian Tags syntax

Able to publish PKM notes

  • Backlinks in each note
  • Graph view for PKM

Hugo

After doing a bit of research the easiest solution I found that could meet all my requirements was to use Hugo in conjunction with some extensions.

Hugo is an open-source static site generator built in Go

  • Has a themes marketplace
  • Themes can provide their own features with custom shortcodes
  • Has native RSS feed support
  • Uses Goldmark for parsing markdown which conforms to CommonMark
  • Optional extensions to the markdown syntax or parsing

Today I Learned(TIL) is a theme for Hugo with nice features for articles and PKM

  • Custom Shortcodes
    • Backlinks and graph view
    • Sidenotes in the gutter
    • Obsidian-like callouts with admonitions

Problems

Implementation

Step 1: Export Obsidian notes

Create an .export-ignore file in the obsidian vault

code snippet start

# file: .export-ignore

/private
metadata.json

# Ignore dirs
/$archive
/$canvases
/$templates
/$projects

# Ignore pdfs
*.pdf

code snippet end

Export the obsidian vault with obsidian-export

sh code snippet start

mkdir exported-pkm

obsidian-export --frontmatter=always ~/pkm exported-pkm

sh code snippet end

Create a python script to replace relative markdown links with TIL template’s backlinks

py code snippet start

import os
import re
import sys

# Check if a path argument was given
if len(sys.argv) < 2:
    print("Usage: python replace_links.py /path/to/content")
    sys.exit(1)

content_dir = sys.argv[1]

# Regex to match markdown links: [text](url)
link_re = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")


def is_absolute_url(url):
    return url.startswith("http://") or url.startswith("https://")


def replace_links_in_file(filepath):
    with open(filepath, "r", encoding="utf-8") as f:
        text = f.read()

    def replacer(match):
        link_text = match.group(1)
        link_path = match.group(2)

        # Only replace if link is NOT absolute and points to a .md file
        if not is_absolute_url(link_path) and link_path.endswith(".md"):
            basename = os.path.splitext(os.path.basename(link_path))[0]
            return f'{{{{-dlt this- < backlink "{basename}" "{link_text}" > -dlt this-}}}}'
        else:
            # Leave unchanged
            return match.group(0)

    new_text = link_re.sub(replacer, text)

    if new_text != text:
        with open(filepath, "w", encoding="utf-8") as f:
            f.write(new_text)
        print(f"Updated links in {filepath}")


for root, _, files in os.walk(content_dir):
    for file in files:
        if file.endswith(".md"):
            replace_links_in_file(os.path.join(root, file))

py code snippet end

Run script

sh code snippet start

python3.11 obs-hugo-link-reformater.py ./exported-pkm/

sh code snippet end

Limitations found
  • obsidian-export fails if the markdown file contains line breaks (---) that are not part of the frontmatter
  • Dataviews do not get exported (obviously)
  • Due to the way backlinks are added, embedding markdown files is not possible
Step 2: Create Hugo site

code snippet start

hugo new site notes-publish-test

code snippet end

Follow the TIL theme set-up guide

Step 3: Copy over content from exported notes

code snippet start

mkdir content/notes
mkdir content/posts
mkdir static/0-attachments

cp exported-pkm/*.md notes-publish-test/content/notes

cp exported-pkm/\$blog/*.md notes-publish-test/content/posts

find exported-pkm/0-attachments/ -maxdepth 1 -type f ! -name '*.md' -exec cp {} notes-publish-test/static/0-attachments/ \;

code snippet end

Step 4: Add custom hook

Using the hooks specified here

But with a slight change for the image hooks! Since all my assets are stored in the static folder of the hugo repo, we have to strip the “notes/” and “posts/” path

code snippet start

{{- $url := urls.Parse .Destination -}}
{{- $scheme := $url.Scheme -}}

<img src="
  {{- if eq $scheme "" -}}
    {{- if strings.HasSuffix $url.Path ".md" -}}
      {{- relref .Page .Destination | safeURL -}}
    {{- else -}}
      {{- $ddir := strings.TrimPrefix "notes/" .Page.File.Dir -}}
      {{- $ddir := strings.TrimPrefix "posts/" $ddir -}}
      {{- printf "/%s%s" $ddir .Destination | safeURL -}}
    {{- end -}}
  {{- else -}}
    {{- .Destination | safeURL -}}
  {{- end -}}"
  {{- with .Title }} title="{{ . | safeHTML }}"{{- end -}}
  {{- with .Text }} alt="{{ . | safeHTML }}"
  {{- end -}}
/>

{{- /* whitespace stripped here to avoid trailing newline in rendered result caused by file EOL */ -}}

code snippet end

Step 5: Add support for callouts

Add the hugo admonitions plugin

code snippet start

[module]
  [[module.imports]]
    path = "github.com/michenriksen/hugo-theme-til"
  [[module.imports]]
    path = "github.com/KKKZOZ/hugo-admonitions"

code snippet end

Step 6: Create obsidian template for inserting frontmatter

code snippet start

---
title: {{title}}
date: {{date:YYYY-MM-DDTHH:mm:ssZ}}
draft: true
---

code snippet end

References: