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:

obsidian-setup-plugins.png

'Hitting save' entails a git commit using:

setup-obsidian-git-commit.png

Followed by git push using:

setup-obsidian-git-push.png

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

MkDocs

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

Customization

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

Hooks

only_include_published.py

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:
            try:
                metadata = frontmatter.load(raw_file).metadata
                if is_page_published(metadata):
                    log.info(f"Adding published document {file.src_uri}")
                else:
                    files.remove(file)
            except:
                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

flatten_filenames.py

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 how-to-write-a-dll-in-rust-part-1.md.

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

Plugins

mkdocs-obsidian includes these third-party plugins:

GitHub

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. peddamat.github.io 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 peddamat.github.io 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.

Repositories

notes repository

This is a private repository containing my Obsidian vault:

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

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

name: Trigger Deployment
on:
  push:
    branches:
      - main

env:
  USER: peddamat
  REPO: mkdocs-obsidian

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - name: Trigger Build and Deploy
        run: |
          curl -X POST https://api.github.com/repos/$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

on:
  repository_dispatch:
  push:
    branches:
      - main

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

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

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          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
        with:
          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
        with:
          personal_token: ${{ secrets.PULL_NOTES }}
          external_repository: peddamat/peddamat.github.io
          publish_branch: main  # default: gh-pages
          publish_dir: ./site
          cname: samrambles.com

peddamat.github.io 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: username.github.io, where username is your GitHub username.

Meaning, my free website, peddamat.github.io, is served up from my peddamat.github.io repository, which is configured like this:

setup-github-pages.png


GitHub Workflow

Permissions

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

github-actions-workflow-perms.png

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 https://github.com/settings/tokens:

setup-pat.png

And select the following scopes:

setup-scopes.png

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:

setup-security.png

Hit 'New repository secret':

setup-secret-token.png

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@github.com:peddamat/mkdocs-obsidian.git samrambles.com
cd samrambles.com
git clone git@github.com:peddamat/notes.git notes
pipenv shell
pipenv install
make serve

And looks like:

site-deployment.gif

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 peddamat.github.io using:

$ mkdocs gh-deploy --force --remote-branch main --remote-name https://peddamat:<personal-access-token>@github.com/peddamat/peddamat.github.io.git

blog-setup-github-down.png


Last update: 2023-05-05