Skip to content

How I Setup This Blog

I recently migrated my, uh... pkm "system" from a Joplin and OneNote mashup to one centered solely around Obsidian.

See, months ago I read about Zettelkasten, which began my foray into the world of roam-likes, starting with logseq and ending with Obsidian.

What actually hooked me was the maturity of its vim bindings - Joplin’s are great, logseq’s feel clunky.

10000-ft View

I write notes in Obsidian. When I hit save, my changes are pushed to GitHub, which rebuilds and redeploys my site.

It looks something like this:

Drawing 2023-03-02 09.57.01.excalidraw.svg

  • Write notes in Obsidian
  • Store notes on GitHub
  • Convert notes to HTML with MkDocs
  • GitHub Pages used for hosting

Obsidian Git

Here is my Obsidian setup: the key component being Obsidian Git:


'Hitting save' entails a git commit using:


Followed by git push using:


Obsidian Git auto-generates a commit message, so the above is all it takes for me to deploy.


MkDocs is a static-site generator: it converts Markdown (.md) files into .html, similar to Jekyll and Hugo.

MkDocs-Material is an MkDocs distribution which comes with a pretty theme and a number of compelling extensions which make your site look and feel modern: see here


mkdocs-obsidian is my customized MkDocs-Material configuration, based off mr-karan's repository.

I wrote code to scratch itches, adding:

  • A way to deploy only "Published" notes, keeping "Drafts" private
  • A way to "slugify" filenames so my URLs would be SEO-friendly
  • Fixes for buggy "[[wikistyle]]" link rendering


In lieu of using separate Obsidian vaults, or segregating my notes by subfolder, I prefer to distinguish "Published" notes from drafts through frontmatter metadata.

Specifically, by denoting "Published" notes as those that have the publish key set to true, which looks like:

publish: true

This simple MkDocs hook triggers when on_files fires, reading each note and filtering for notes with frontmatter containing publish: true.

import os, logging, typing, frontmatter
from typing import Optional

import mkdocs.plugins
from mkdocs.structure.pages import Page
from mkdocs.structure.files import Files
from mkdocs.config.defaults import MkDocsConfig

log = logging.getLogger('mkdocs')

def is_page_published(meta: typing.Dict) -> bool:
    if 'publish' in meta:
        return meta['publish'] == True

def on_files(files: Files, *, config: MkDocsConfig) -> Optional[Files]:
    base_docs_url = config["docs_dir"]

    for file in files.documentation_pages():
        abs_path = os.path.join(base_docs_url, file.src_uri)
        with open(abs_path, 'r') as raw_file:
                metadata = frontmatter.load(raw_file).metadata
                if is_page_published(metadata):
          "Adding published document {file.src_uri}")
                log.error(f"Found malformed frontmatter in {file.src_uri}!")

    return files

def on_post_page(output: str, *, page: Page, config: MkDocsConfig) -> Optional[str]:
    if not is_page_published(page.meta):
        return ''

    return output

This is even simpler MkDocs hook triggers when on_files fires, converting filenames such as How To Write A Dll In Rust (part 1).md to

import logging, re
import mkdocs.plugins

log = logging.getLogger('mkdocs')

# Rewrite using `python-slugify`
def on_files(files, config):
    for f in files:
        if f.is_documentation_page() or f.is_media_file():
            f.abs_dest_path = f.abs_dest_path.replace(" ", "-").lower()
            f.abs_dest_path = f.abs_dest_path.replace("(", "").lower()
            f.abs_dest_path = f.abs_dest_path.replace(")", "").lower()
            f.dest_path = f.dest_path.replace(" ", "-").lower()
            f.dest_path = f.dest_path.replace("(", "").lower()
            f.dest_path = f.dest_path.replace(")", "").lower()
            f.url = f.dest_path.replace("%20", "-").lower()

    return files


mkdocs-obsidian includes these third-party plugins:


My setup uses three repositories:

  1. notes is a private repo containing my "Obsidian vault" (folder containing .md files)
  2. mkdocs-obsidian is a public repo containing my MkDocs configuration (mkdocs.yml)
  3. is a public repo containing static .html files

When changes are pushed to the notes repository, the mkdocs-obsidian repository rebuilds all "Published" notes and pushes the results to the repository.

This is all orchestrated using GitHub Action workflows, one in the notes repository and one in the mkdocs-obsidian repository, which we'll discuss below.


notes repository

This is a private repository containing my Obsidian vault:

$ tree
├── .github
   ├── workflows
|      └── main.yml # (1)
├── .obsidian
├── Coding
   ├── Creating A DLL With
├── Dotfiles
├── assets
├── img
├── resources
├── stylesheets
GitHub Workflow

The GitHub Action workflow is defined in .github/workflows/main.yml.

name: Trigger Deployment
      - main

  USER: peddamat
  REPO: mkdocs-obsidian

    runs-on: ubuntu-latest

      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - name: Trigger Build and Deploy
        run: |
          curl -X POST$USER/$REPO/dispatches \
          -H 'Accept: application/vnd.github.everest-preview+json' \
          -u ${{ secrets.API_TOKEN_GITHUB }} \
          --data '{"event_type": "Trigger Workflow", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
      - uses: actions/checkout@v3

The "Trigger Deployment" workflow, runs after commits are pushed to main branch of the notes repo, its sole purpose being to triggers the "Build and Deploy Site" workflow in the mkdocs-obsidian repository.

mkdocs-obsidian repository

This is a public repository containing my fork of mr-karan/notes:

$ tree -L 1 -a
├── .github
   ├── workflows
|      └── main.yml
├── Makefile
├── mkdocs.yml
├── hooks
├── overrides
├── Pipfile
GitHub Workflow
name: Build and Deploy Site

      - main

    runs-on: ubuntu-latest
      - name: Checkout mkdocs-obsidian repo
        uses: actions/checkout@v3

      - name: Checkout notes repo into ./notes
        uses: actions/checkout@v3
          token:  ${{ secrets.PULL_NOTES }}
          repository: peddamat/notes
          path: notes

      - name: Setup Python
        uses: actions/setup-python@v4
          python-version: '3.8'

      - name: Upgrade pip
        run: |
          # install pip=>20.1 to use "pip cache dir"
          python3 -m pip install --upgrade pip

      - name: Get pip cache dir
        id: pip-cache
        run: echo "::set-output name=dir::$(pip cache dir)"

      - name: Cache dependencies
        uses: actions/cache@v2
          path: ${{ steps.pip-cache.outputs.dir }}
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install dependencies
        run: python3 -m pip install -r ./requirements.txt

      - run: mkdocs build

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
          personal_token: ${{ secrets.PULL_NOTES }}
          external_repository: peddamat/
          publish_branch: main  # default: gh-pages
          publish_dir: ./site
          cname: repository

This is a public repository containing the output of MkDocs, a static .html render of my .md notes.

GitHub Pages

GitHub Pages provides free top-level domain hosting for one site, with the stipulation being the site must be hosted in a repository named:, where username is your GitHub username.

Meaning, my free website,, is served up from my repository, which is configured like this:


GitHub Workflow


My notes and mkdocs-obsidian repositories need the following workflow permissions, found under "Settings / Actions / General" in each repository:


Personal Access Token (PAT)

Personal access tokens function like ordinary OAuth access tokens. They can be used instead of a password for Git over HTTPS, or can be used to authenticate to the API over Basic Authentication.

The workflows used by the notes and mkdocs-obsidian repositories utilize PATs to allow inter-repository interaction, for example, allowing the notes repository to trigger workflows in the mkdocs-obsidian repository.

To generate a PAT, click "Generate new token (classic)" on


And select the following scopes:


Repository Secret

"Repository Secrets" enable passwords/tokens to be used by workflows without requiring them to be publically visible in workflow .yaml.

Both the "Trigger Deployment" and "Build and Deploy Site" workflows use a secret named secrets.API_TOKEN_GITHUB, contains a "GitHub Personal Access Token (PAT)", configured as described in the Personal Access Token (PAT) section.

Create the secret.API_TOKEN_GITHUB secret in each repository's "Settings / Secrets and variables / Actions" section:


Hit 'New repository secret':


And paste the PAT from Step 1.

MkDocs Development Setup

To work on the MkDocs code, I use WSL2 running Ubuntu under Windows 10, with VSCode as my editor.

Setting up an development environment consists of the following steps:

  • Clone mkdocs-obsidian repo
  • Clone notes repo
  • Install dependencies
  • Start staging server

Which translates to:

git clone
git clone notes
pipenv shell
pipenv install
make serve

And looks like:


Adding MkDocs Plugins

MkDocs plugins are generally distributed as Python PyPi packages, installed using pip, for example:

Installing the mkdocs-git-revision-date-plugin:

pip install mkdocs-git-revision-date-plugin

Since I am using pipenv, this would look like:

pipenv install mkdocs-git-revision-date-plugin

Deploying Directly

Sometimes GitHub shits the bed, when that happens, I can manually deploy to using:

$ mkdocs gh-deploy --force --remote-branch main --remote-name https://peddamat:<personal-access-token>


Last update: 2023-05-05