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
- Unable to use WikiLinks by default -> fixed with obsidian-export
- Does not support Obsidians’ callout/admonition syntax -> fixed with hugo-admonitions
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
- 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