How to setup a blog for free

June 17, 2025

TechnologyObsidianComputer Science

Note: As of 2026, this site is set up differently. Perhaps i will write a follow-up.

This is how I set up this Blog, free of charge. All you need is a markdown editor, in my case Obsidian, and a Github account. We'll be able to edit the blog in markdown locally, and then create a static web page on Github Pages. You can keep your other notes seperated from the blog, we will only publish the <Vault>/Blog folder. We'll automate the publishing stage with a CI/CD pipeline and an Obsidian plugin. You'll be able to publish just by pressing a hotkey in Obsidian!

Technologies

Besides Obsidian and Github Pages, a static site generator is used to create the HTML sources. We will use Quartz 4, which comes pre-configured with useful features, like an RSS-feed and a sitemap.

Setup Quartz

We will clone the current Quartz repository into our vault. This allows for customization of components and plugins. If you want to use a submodule, keep in mind to use --depth=2 later when cloning the repo. Otherwise just delete the nested .git folder. (Do not delete your main Git folder.) We will clone Quartz into the folder .github/quartz.

001mkdir .github && cd .github
002git clone https://github.com/jackyzha0/Quartz.git
003cd quartz
004npm i
005npx quartz create

In .github/Quartz/Quartz.config.ts, you should definitely change the fields

  1. pageTitle
  2. baseUrl

For further customization, refer to the documentation.

Setup Github Pages

We will host the website in a separate Github repository for security-reasons. Create this repository, and then create an access token, so we can publish from the main repository. Upload the token as a secret in the main repository, that we can use in the pipeline later. There it's safely stored. You will need to create a new token, if you decide to create a new pipeline however. Go to Settings > Secrets > Actions and add the secret under the PAGES_DEPLOY_TOKEN. Lastly, enable Github Pages in the hosting-repository under Settings > Pages.

We can create the main CI/CD pipeline now. Github Actions Pipelines are stored in .github/workflows.

001name: Build and Deploy Quartz to External Pages Repo
002
003on:
004 push:
005 branches: [ "*" ]
006
007jobs:
008 build:
009 runs-on: ubuntu-latest
010
011 steps:
012 - name: Checkout main repo
013 uses: actions/checkout@v4
014
015 - name: Setup Node.js 22
016 uses: actions/setup-node@v4
017 with:
018 node-version: 22
019
020 - name: Replace Quartz content with Blog folder
021 run: |
022 rm -rf .github/quartz/content/*
023 mkdir -p quartz/content
024 rsync -av --delete Blog/ .github/quartz/content/
025
026 - name: Install dependencies
027 working-directory: .github/quartz
028 run: npm install
029
030 - name: Build Quartz site
031 working-directory: .github/quartz
032 run: npx Quartz build
033
034 - name: Push to pages repo
035 run: |
036 ls
037 cd .github/quartz/public
038 git init
039 git config user.name "github-actions[bot]"
040 git config user.email "github-actions[bot]@users.noreply.github.com"
041 git remote add origin https://x-access-token:${{ secrets.PAGES_DEPLOY_TOKEN }}@github.com/fabsch225/fabsch225.github.io.git
042 git checkout -b main
043 git add .
044 git commit -m "Deploy Quartz site"
045 git push -f origin main
046

Improvements to the setup

Git-integration for Obsidian (desktop)

To easily push the blog from the Obsidian user interface, we can configure a plugin for Obsidian. This is not absolutely necessary, but speeds up your workflow. Also, this will serve as a practical back-up solution for your whole Vault. Disclaimer. Git needs to be installed on your machine, so this is only relevant for PC and Mac.

Obsidian plugins live in your vault, specifically in the .obsidian folder. We'll just create it there. Once we start using git, version control for our custom setup comes for free. We require the following folder-structure:

001.Obsidian/
002└── plugins/
003 └── git-automation/
004 ├── main.js
005 └── manifest.json

manifest.json will tell Obsidian the plugin metadata:

001{
002 "id": "git-auto-push",
003 "name": "Git Auto Push",
004 "version": "1.0.0",
005 "minAppVersion": "0.12.0",
006 "description": "Automatically add, commit, and push your vault",
007 "author": "Fabian Schuller",
008 "authorUrl": "https://github.com/fabsch225",
009 "main": "main.js"
010}

In main.js, we configure the Obsidian-command, that will trigger the publishing process, and start a node child-process, that executes the git commands.

001const { Plugin, Notice } = require('obsidian');
002const { exec } = require('child_process');
003
004module.exports = class GitAutoPushPlugin extends Plugin {
005 async onload() {
006 this.addCommand({
007 id: 'git-auto-commit-push',
008 name: 'Git Add, Commit (Date), and Push',
009 callback: () => this.runGitCommands()
010 });
011 }
012
013 async runGitCommands() {
014 const vaultPath = this.app.vault.adapter.getBasePath();
015 const date = new Date().toISOString().split('T')[0];
016
017 const command = `
018 cd "${vaultPath}" && \
019 git add . && \
020 git commit -m "${date}" && \
021 git push
022 `;
023
024 exec(command, (error, stdout, stderr) => {
025 if (error) {
026 console.error('Git command error:', error);
027 new Notice('Git push failed: ' + error.message);
028 return;
029 }
030 if (stderr) console.warn('Git stderr:', stderr);
031 new Notice('Git commit and push successful!');
032 });
033 }
034}
035

Now, reload the plugins in the Obsidian app, and enable our custom plugin. Then press CTRL+P to open the command-prompt and search for the command Automatically add, commit, and push your vault. In the plugin's settings, you can also bind a hotkey to this command, if you so wish.

Strip link prefixes

If you made it this far, you will notice that the Links between pages are broken. They each have the prefix Blog/, because it's like this in your main Obsidian Vault. For this, i propose a custom Quartz plugin. We will create a plugin, that transforms the links, so the Prefix is avoided. This only requires a regex to find those Links, and a search-and-replace Library to actually transform the pages.

Plugins are not difficult to set up, and this simple plugin opens the way for more advanced customization. Plugins in Quartz fall into 3 categories

Plugin Type
Functionality
Exemplary Use-Case
TransformerAlter or enrich each file by manipulating text, AST or resourcesRender latex or create wordcount
FilterDecide which file gets publishedSkip drafts
EmitterProduce final output (override default here)Custom layout

We will use a transformer:

001import { QuartzTransformerPlugin } from "../types"
002import { findAndReplace } from "mdast-util-find-and-replace"
003import { Text } from "mdast"
004
005interface Options {
006 prefix: string
007}
008
009export const StripPrefixLinks: QuartzTransformerPlugin<Options> = (opts?: Options) => {
010 const prefix = opts?.prefix ?? ""
011
012 return {
013 name: "StripPrefixLinks",
014 markdownPlugins() {
015 return [
016 () => {
017 return (tree) => {
018 const pattern = new RegExp(`\\[\\[${prefix}/([^\\]]+?)\\]\\]`, "g")
019
020 findAndReplace(tree, [
021 [
022 pattern,
023 (_match: string, captured: string) =>
024 ({ type: "text", value: `[${captured}](${captured})` } as Text),
025 ],
026 ])
027 }
028 },
029 ]
030 },
031 }
032}
033

We will place this at .github/Quartz/Quartz/plugins/transformers/stripPrefixLinks.ts. Now, we'll have to register the Plugin at ... /plugins/transformers/index.ts by adding the Line

001export { StripPrefixLinks } from "./stripPrefixLinks.ts"

Now, we enable the plugin in the config .github/Quartz/Quartz.config.ts, by adding it in front of the other transformer plugins:

001//...
002plugins: {
003 transformers: [
004 Plugin.StripPrefixLinks({ prefix: "Blog" }),
005 //...
006 ]
007}, //...

If you want to setup the Blog elsewhere in your Obsidian vault, also modify the prefix here.

Custom footer

To create a custom footer, edit .github/quartz/quartz/components/Footer.tsx. If you just want to add custom links (like to your social media) to the footer, you can change those in .github/quartz/quartz/quartz.layout.tsx.