obsidian-blog
is a simple static site generator inspired by Jekyll and written in Python. It allows you to use your atomic notes to compile static pages and posts as MoCs. Sounds interesting, isn't it?
Need to mention, obsidian-blog
is not a one-click-ready-to-go blog. It requires some basic codding skills to tinker a bit with handlebars and css to cook what you need.
The simplest way is to run:
pip install obsidian-blog
The simplest way to start so far is to copy .blog
dir from this repository and tweak css and templates to fit your design.
For writing purpose, you can start obsidian-blog
to watch and serve mode:
obsidian-blog --serve --watch
This one will start a simple http server on the localhost:4200
and rebuild your content on any change.
To build your notes into the static files just run obsidian-blog
command. Static site will be in the .build
directory.
The first thing you need to do is to create a _blog
directory in the root of your vault with a structure like on the listing below. The simplest way is to copy it from this example vault.
_blog
├── Post.md
├── _assets
│ └── styles.css
├── _layouts
│ └── main.hbs
└── _pages
├── about.hbs
└── index.hbs
Let me briefly explain each directory inside of _blog
:
_assets
are static files, such as css
, js
or images are always being copied into resulting build._layouts
is a room for handlebars layout files. You can specify different layouts for diferent pages and posts by setting layout: <layout_name>
in the [yaml-frontmatter][frontmatter] part of the file._pages
is where handlebars page templates live. Like about
, contacts
, all-posts
, my cv
and so on.Note, that there is no posts
directory, because, all your posts lives right inside the _blog
directory.
obsidian-blog
supports .env
, and it seems a pretty good place to define your blog_title
and other specific settings. A list of variables is here.
Another way is to use cli flags. Take a look on obsidian-blog --help
for a list of supported options:
notes ❯ obsidian-blog --help
obsidian-blog
Static site generator for obsidian.md notes.
Usage:
obsidian-blog [-d] [-w] [-s] [--port <number>] [--title <string>] [--posts_dir <directory>] [--pages_dir <directory>]
Options:
-h --help Show this screen.
-w --watch Enable watcher
-s --serve Enable web-server
-p --port=<number> Web-server port [default: 4200]
-d --drafts Render draft pages and posts
--title=<string> Blog title [default: My Blog]
--version Show version.
Parser starts at 2 main directories: Pages
and Posts
. It recursively processes all embedded notes if they are placed on the new line and don't have any text around. Parser also supports cycles detection, and shouldn't fail in this case.
Parser recursively collects all entities (such as images, notes, links, etc) from your posts and pages and build a flat list of them.
During the build, obsidian-blog
takes all local files used in your posts and pages, copies them into .build
directory and updates their links accordingly.
obsidian-blog
renders a header with note title
for each note with a non-empty content. This header has id
attribute and can be used as an anchor.
As it was mentioned before, there are plenty of ways to define titles, and you can even skip it to use filename instead.
Both posts and pages are post-processed by handlebars
.
obsidian-blog
don't publish anything explicitly marked as published in the frontmatter section:
---
published: True
---
Non-published notes are still being parsed, but ignored during the rendering.
There is also the draft feature. You can annotate your post draft: True
to make it visible if you start obsidian-blog
with the --drafts
flag.
obsidian-blog
has a title-detection mechanism:
[[this | one]]
, the one
will be a titletitle
attribute in the frontmatter section, it will be used.If note has no other title then a filename, and filename contains the delimeter (-
), the rightest section will be used as a title. This helps if you name your notes with a category prefix, like Prometheus - PromQL Selectors.md
becomes PromQL Selectors
. It's a bit opinionated, but this convention saves time.
Dev mode is useful to write and check your drafts locally:
obsidian-blog -dws
If a note has links
list in the frontmatter, this links will be rendered just under the note as the Further Reading block
links:
- name: Example
url: https://example.com
Further Reading:
Another opinionated thing: obsidian-blog
doesn't consider obsidian links that has text around as notes that should be unwrapped into the post. It seems natural (at least for me), that this links should be processed as links.
obsidian-blog
checks if the notes have link
attribute in the frontmatter section, and if they do, it replaces this includes as old good html links.
That's really useful on a large vault, when you links to a MoC in a specific note, and you don't want the whole MoC to be populated into your post.
global_context
is always accessible in all handlebars templates (layouts, pages) and includes few variables:
{
"config": Config
"self": Page, # refers to the active page
"layouts": dict[str, Layout],
"posts": list[Page], # list of posts
"pages": list[Page], # list of pages
})
Config is passed to handlebars templates as config
variable.
Further Reading:
Layouts are old good handlebars layouts compiled with a global_context
and page_context
or post_context
accordingly. There is a simple example layout renders links to all pages and a content.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ self.title }}</title>
<link rel="stylesheet" href="{{ config.public_dir }}/styles.css">
</head>
<body>
<h1 class="menu__header">
<a href=/>{{ config.blog_title }}</a>
</h1>
<div class="menu">
<ul class="links">
{{#each pages}}
<li class="link">
<a href="/{{this.slug}}">{{this.title}}</a>
</li>
{{/each}}
</ul>
</div>
<div class="content">
<div class="page">
{{{ content }}}
</div>
</div>
</body>
</html>
In a post or a page, you can specify which one you'd like to use to render the content within the yaml-frontmatter block:
---
title: Hello World
date: 2021-01-09
layout: main
---
If layout is not specified, main
will be used as default one.
To get some data from the current page rendered in a handlebars template, you can refere to self
variable, which has properties listed below:
self.content
refers to the rendered html content of the pageself.entities
refers to the list of entities parsed from a page and its children. That's useful to render ToCs, etcself.title
self.date
self.slug
is a path after /
the page is published toself.meta
is a dict contains frontmatter variables of the pagePages are handlebars templates support a yaml-frontmatter section. Pages stands for anything but posts.
This is an example of index.hbs
page renders all posts from the blog:
---
title: Posts
---
<h1>{{ self.title }}</h1>
<ul>
{{#each posts}}
<li>
<span>[{{ this.date }}]: </span>
<a href="/{{this.slug}}">{{ this.title }}</a>
</li>
{{/each}}
</ul>
Further Reading:
Images are processed in 2 steps:
All images are parsed into asset_entity
objects contains their placeholders, alts, and urls.
During the build image files are copied under .build
dir, and urls are updated accordingly. Then images are rendered into HTML.
To point to a specific image, use config.public_dir
prefix in a handlebars template.
It's quite easy to build and deploy a static site to your own domain:
name: Deploy GitHub Pages
on:
push:
branches:
- "master"
env:
PYTHON_VERSION: 3.10.2
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install obsidian-blog
run: pip install obsidian-blog
- name: build static site
run: obsidian-blog
- name: Add gh-pages CNAME
run: echo ${{ secrets.DOMAIN }} > .build/CNAME
- uses: JamesIves/github-pages-deploy-action@4.1.4
with:
branch: gh-pages
folder: .build
Further Reading: