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:
- 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¶
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:
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:
- mkdocs-roamlinks-plugin: converts
[[wikilinks]]
links to proper website links - mkdocs-blogging-plugin: generates the homepage article listing
- mkdocs-git-revision-date-plugin: adds "Last updated" footer tags
GitHub¶
My setup uses three repositories:
- notes is a private repo containing my "Obsidian vault" (folder containing
.md
files) - mkdocs-obsidian is a public repo containing my MkDocs configuration (mkdocs.yml)
- 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:
GitHub Workflow¶
Permissions¶
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 https://github.com/settings/tokens:
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@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:
Adding MkDocs Plugins¶
MkDocs plugins are generally distributed as Python PyPi packages, installed using pip
, for example:
Installing the mkdocs-git-revision-date-plugin
:
Since I am using pipenv
, this would look like:
Deploying Directly¶
Sometimes GitHub shits the bed, when that happens, I can manually deploy to peddamat.github.io
using: