Blog Workflow: Obsidian, Forgejo, Quartz, and Cloudflare Pages
8 min read
In this post, we’ll walk through a modern workflow for creating and publishing content using Obsidian, Forgejo, Quartz, and Cloudflare Pages. This setup allows you to maintain a private knowledge base while selectively publishing specific blog posts to a fast, global edge network.
The Components
Obsidian: Our primary interface for writing notes and blog posts.
Forgejo: A self-hosted Git repository service that hosts our content and provides Git runners for CI/CD.
Quartz: A fast and customizable static site generator specifically designed for Obsidian vaults.
Cloudflare Pages: The platform for publishing and hosting the generated static site.
The Workflow
graph TD
subgraph "Content Creation (Manual)"
direction LR
A[Obsidian: Write Post] --> B[Set status: published] --> C[Forgejo: Push Changes]
end
C -- Triggers --> D[Forgejo Runner]
subgraph "Publishing Pipeline (Automated)"
direction LR
E[Filter status: published] --> F[Apply Custom Configs] --> G[Quartz Build] --> H[Cloudflare Deploy]
end
D --> E
1. Planning and Writing in Obsidian
Manage your content calendar using a dynamic board within Obsidian. Create your posts in blog/. While drafting, you can use statuses like status: idea or status: draft. When you’re ready for the world to see a post, simply update its property to status: published.
2. Push to Forgejo
Push your changes to your Forgejo repository. This can be done via the Obsidian Git plugin. Once the push is received, Forgejo triggers the runner to begin the publishing pipeline.
3. Publishing the Static Site
The Forgejo runner automates the remaining steps:
Filter & Sync: It scans the blog/ folder and identifies only those posts with status: published.
Clean URLs: It syncs these posts to the Quartz engine, renaming main files to index.md.
Customization: It applies your custom styles and layouts from quartz-custom/.
Build & Deploy: It executes the Quartz build and uses Wrangler to deploy to Cloudflare Pages.
Detailed Configuration
1. Initial Repository Setup
The first step is to create a home for your Obsidian vault and blog content.
Create a Repository in Forgejo: Log in to your Forgejo instance and create a new repository (e.g., my-blog). Keep it private if you want your notes to stay secure.
Clone Locally: Open your terminal and clone the empty repository to your machine:
Note: We check in the rest of the .obsidian folder to ensure your plugins (like Bases and Obsidian Git) and their settings are preserved across your devices.
2. Quartz as a Subtree
Integrate the Quartz engine into your repository to handle the static site generation. This places the Quartz source code in a subdirectory while allowing you to pull updates easily.
To keep everything organized and compatible with the runner script, you need to set up a specific directory structure. Run the following command from the root of your repository:
mkdir blog quartz-custom planning
This creates the following structure:
quartz-engine/: Quartz engine (Git subtree).
quartz-custom/: Place your custom configuration files here (e.g., quartz.config.ts).
planning/: Internal planning and Kanban boards (Excluded from build).
blog/: This is where all Obsidian notes and blog posts are stored.
Each blog post has its own subdirectory: blog/hello-world/.
The post itself is a Markdown file: blog/hello-world/hello-world.md.
Assets are stored in: blog/hello-world/assets/.
Note: For the homepage of your site, create a folder at blog/index/ with a file named index.md marked with status: published.
4. Customizing Quartz
Since the quartz-engine directory is managed as a subtree, you should not modify files inside it directly. Instead, maintain a quartz-custom/ directory at the root of your repository with the same structure as the Quartz project.
To get started with your customization:
Copy the default configuration files from the engine to your custom directory:
quartz-custom/quartz.config.ts: Controls the site title, URL, theme colors, and plugins.
quartz-custom/quartz.layout.ts: Defines the placement of components like the explorer, graph, and backlinks.
quartz-custom/quartz/styles/custom.scss: For adding custom CSS/Sass overrides.
quartz-custom/static/: For custom assets like favicons or images.
These files will be automatically copied into the engine by the Forgejo runner during the build process, overwriting the defaults.
5. Obsidian Git Configuration
To automate the sync process from your local vault, the Obsidian Git plugin is essential.
Install the Plugin: In Obsidian, go to Settings > Community plugins > Browse and search for “Git”.
Authentication: Ensure you have an SSH key configured or use a Personal Access Token. Configure your username and email in the plugin settings.
Automatic Backups:
Set the Vault backup interval (e.g., every 10 minutes) to automatically commit and push your changes.
Enable Pull updates on startup to ensure your local vault is always in sync with Forgejo.
6. Forgejo Secrets and Variables
Before configuring the runner, set up your credentials in Forgejo under Settings > Actions:
Secrets: Add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
Variables: Add CLOUDFLARE_PROJECT_NAME.
7. Forgejo Runner Workflow (Updated)
The Forgejo runner handles the logic of filtering your notes, applying customizations, and deploying. Create .forgejo/workflows/publish.yml:
name: Publish Blogon: push: branches: - mainjobs: build-and-deploy: runs-on: docker container: image: node:22-slim steps: - name: Checkout Repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Selective Sync and Build run: | # 1. Prepare Quartz cd quartz-engine npm ci rm -rf content/* # 2. Apply Custom Configurations cd .. if [ -d "quartz-custom" ]; then echo "Applying custom configurations..." cp -rv quartz-custom/* quartz-engine/ fi # 3. Filter and Sync Blog Posts for post_dir in blog/*/ ; do post_dir=${post_dir%/} post_name=$(basename "$post_dir") md_file="$post_dir/$post_name.md" if [ "$post_name" = "index" ]; then target_file="quartz-engine/content/index.md" else target_file="quartz-engine/content/$post_name/index.md" fi if [ -f "$md_file" ] && grep -q "^status: published" "$md_file"; then echo "Syncing $post_name..." if [ "$post_name" = "index" ]; then cp -r "$post_dir/"* "quartz-engine/content/" else mkdir -p "quartz-engine/content/$post_name" cp -r "$post_dir/"* "quartz-engine/content/$post_name/" fi mv "quartz-engine/content/$post_name/$post_name.md" "$target_file" 2>/dev/null || true fi done - name: Build Static Site run: | cd quartz-engine npx quartz build - name: Deploy to Cloudflare Pages env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | npx wrangler pages deploy quartz-engine/public --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}"
Creating Blog Posts in Obsidian
Creating new blog posts in Obisidan is as simple as creating a directory under blog/ with a name stub for the post and then creating a new note with the same name stub.
1. The status Property
We use a status property in the YAML frontmatter to control visibility and track the lifecycle of each post:
idea: For initial concepts and brainstormed topics.
research: Used when you are actively gathering data and references.
draft: For posts currently being written.
review: For finished drafts that need a final proofread or feedback.
published: The only status that triggers the Forgejo runner to sync to the live site.
---title: "My New Post"status: drafting---
2. Unlisted Posts
For posts you want to publish but keep hidden from the main site navigation and search index (making them accessible only via direct link), use the unlisted and robots properties:
unlisted: true: Hides the post from the Explorer sidebar and internal search.
robots: noindex: Prevents external search engines from indexing the page.
3. Bidirectional Planning with Obsidian Bases
To automate your planning workflow with bidirectional synchronization, we use the Obsidian Bases plugin in conjunction with a Kanban view extension (like Kanban Bases View).
Install the Plugins: In the Community plugins gallery, install:
Bases: The core database engine.
Kanban Bases View: The visual board extension for Bases.
Create a New Base: Select Bases: Create new base from the command palette. Name it Content Calendar and save it in planning/.
Configure the Base: Set the source to the blog/ folder and ensure the status property is tracked.
Setup the Kanban View: In the Base settings, add a new View, select Kanban, group by status, and map your columns to the status values defined above.
Incorporate a collaborative review process using Forgejo Pull Requests and AI Agents:
AI Code Review: Use a Forgejo Webhook to trigger an AI Agent whenever a new draft branch is created. The agent can review the Markdown for grammar, style, and SEO, leaving comments directly on the Pull Request.
Visual Validation Agent: Extend the CI/CD pipeline to deploy a Cloudflare Preview URL for every PR. An AI agent (using a headless browser) can navigate to the preview link to ensure the page renders correctly and no layout shifts or broken images are present before merging to main.