I’ve been wanting a seamless blogging workflow for years. Something where I could just open my note-taking app, write, and have it automatically appear on my portfolio. After some experimentation, I finally cracked it using Obsidian + Git + Astro.
This post documents the entire setup - from why I chose this approach to the exact configuration details. Let’s dive in.

The Problem with Traditional Blogging
As developers, we often overengineer things. Writing a blog post shouldn’t require:
- Opening VS Code (or any code editor)
- Creating a new
.mdfile - Manually typing frontmatter YAML
- Running git add, commit, push commands
- Waiting for CI/CD to deploy
That’s at least 5 steps every single time. For a lazy developer like me, that’s 5 too many.
What I wanted was:
- Single app to write in
- Templates so I don’t forget frontmatter
- Auto-publish on save
- Draft mode for work-in-progress posts
The Architecture
Here’s how the pieces fit together:
flowchart LR
subgraph Writer[" "]
direction TB
O[Obsidian]
T[Template]
G[Obsidian Git]
end
O --> T
T --> G
subgraph Infrastructure[" "]
direction LR
GH[GitHub] --> CP[Cloudflare Pages]
CP --> A[Astro]
end
G -->|push| GH
GH -->|deploy| CP
CP -->|build| A
style Writer fill:#f0f9ff,stroke:#3b82f6,stroke-width:2px
style Infrastructure fill:#f0fdf4,stroke:#22c55e,stroke-width:2px
style O fill:#e0f2fe,stroke:#0284c7
style G fill:#e0f2fe,stroke:#0284c7
style GH fill:#dcfce7,stroke:#16a34a
style CP fill:#dcfce7,stroke:#16a34a
style A fill:#dcfce7,stroke:#16a34a
The stack:
- Obsidian - Local-first markdown editor
- Obsidian Git - Auto-commits and pushes changes
- GitHub - Hosts the repository
- Astro - Static site generator with content collections
Step-by-Step Setup
Step 1: Configure Obsidian as Your Editor
First, you need to point your Obsidian vault to your blog’s content folder:
- Open Obsidian → Open another vault
- Navigate to your portfolio’s root folder (not just
/src/content/post/) - This is critical - the
.gitfolder must be accessible
The vault structure should look like:
portfolio-v2/
├── .git/
├── src/
│ └── content/
│ └── post/
│ ├── post-1.md
│ ├── post-2.md
│ └── Template.md
└── package.json
Step 2: Install and Configure Obsidian Git
Install the Obsidian Git community plugin, then configure these settings:
| Setting | Value | Purpose |
|---|---|---|
| Auto commit | ON | Automatically save changes |
| Auto push | ON | Push to remote immediately |
| Commit message on timer | ON (5 min) | Fallback if no manual save |
| Pull on startup | ON | Sync changes from other devices |
Step 3: Create a Post Template
Create src/content/post/Template.md with all required frontmatter:
---
title: Your Post Title Here
description: A brief summary (1-2 sentences) for the meta description.
dateFormatted: Apr 29, 2026
tags: ["tag1", "tag2"]
published: true
image: your-og-image.png
meta-icon: icon-name
featured: false
---
The key field is published - set it to false while writing, then flip to true when ready.
Step 4: Astro Content Collections Schema
Your posts are validated against this schema in src/content/config.js:
const postCollection = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
dateFormatted: z.string(),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
banner: z.string().optional(),
author: z.string().optional(),
featured: z.boolean().optional(),
published: z.boolean().optional(),
}),
});
The published filter ensures unpublished drafts never hit production:
// Only fetch published posts
const allPosts = await getCollection("post",
({ data }) => data.published !== false
);
Advanced Features
Here’s my actual workflow:
sequenceDiagram
participant You
participant Obs as Obsidian
participant Git as Obsidian Git
participant GH as GitHub
participant CF as Cloudflare
Note over You,CF: Writing a new post
You->>Obs: Write content (Template.md)
Note over Obs: Set published: false
You->>Obs: Edit & refine post
Obs->>Git: Auto-commit (every 5 min)
Git->>GH: Auto-push to remote
You->>Obs: Set published: true
Obs->>Git: Commit & push
Git->>GH: Push changes
GH->>CF: Trigger deployment
CF->>CF: Build Astro site
CF-->>You: Post is live!
Note over You,CF: Total time: ~5 minutes
Auto-Generated Open Graph Images
Every post automatically gets an OG image generated using Cloudflare Workers and Bun. The banner includes:
- Post title
- Description preview
- Tags
- Author name
- Reading time estimate

This happens automatically during the build process - no manual work needed.
Related Posts Algorithm
The related-posts.astro component finds relevant posts by:
- Matching tags from current post
- Sorting by number of matching tags
- Falling back to recent posts if no matches
const relatedPosts = allPosts
.filter(post => post.id !== currentPostSlug)
.map(post => {
const matchingTags = post.data.tags?.filter(
tag => currentTags.includes(tag)
);
return { ...post, matchingTagsCount: matchingTags?.length };
})
.sort((a, b) => b.matchingTagsCount - a.matchingTagsCount)
.slice(0, 3);
Syntax Highlighting
Code blocks use Shiki with a custom theme:
// Example from my actual code
function parseDate(dateStr) {
const [month, day, year] = dateStr.split(" ");
return new Date(`${month} ${Number.parseInt(day)}, ${year}`);
}
const sortedPosts = allPosts
.map(post => ({
...post,
date: parseDate(post.data.dateFormatted),
}))
.sort((a, b) => b.date.getTime() - a.date.getTime());
RSS Feed Integration
Every post automatically appears in the RSS feed at /rss.xml:
<item>
<title>How I Automated My Blog with Obsidian + Git</title>
<link>https://kodo.sh/post/my-auto-blog-setup</link>
<description>A comprehensive guide to setting up...</description>
<pubDate>Tue, 29 Apr 2026 00:00:00 GMT</pubDate>
</item>
Performance Metrics
This setup delivers excellent performance:
| Metric | Score |
|---|---|
| Lighthouse Performance | 98-100 |
| First Contentful Paint | ~300ms |
| Time to Interactive | <500ms |
| Total Page Weight | <50KB (gzipped) |
The secret? Astro ships zero JavaScript for static content by default.
Workflow in Action
Here’s my actual writing process:
- Monday 9:00 AM - Open Obsidian, copy Template.md to
new-post.md - Monday 9:15 AM - Write content, set
published: false - Monday 6:00 PM - Edit, refine, flip
published: true - Monday 6:05 PM - Obsidian Git auto-pushes
- Monday 6:10 PM - Cloudflare Pages deploys
- Monday 6:12 PM - Post is live
Zero friction. No build commands. No remembering YAML schemas.
Troubleshooting
Common issues and fixes:
Posts Not Appearing
- Check
published: truein frontmatter - Verify file is in
src/content/post/ - Run
pnpm devto see validation errors
Git Not Committing
- Confirm vault opens at repo root (has
.gitfolder) - Check Obsidian Git plugin is enabled
- Verify auto-commit settings
Build Errors
- Frontmatter must be valid YAML
- Required fields:
title,description,dateFormatted - Tags must be an array:
tags: ["a", "b"]
What’s Next
This setup has been a game-changer. But I’m already thinking about improvements:
- Automated social posting - Trigger Twitter/LinkedIn posts on publish
- Analytics integration - Track which posts perform best
- Newsletter sync - Automatically add to email list
- AI-assisted writing - Summarize long posts with TL;DR sections
If you want me to cover any of these in a future post, let me know!
Conclusion
The best blogging system is one you’ll actually use. This Obsidian + Git + Astro setup has dramatically lowered the barrier to writing. No more “I’ll post later” - I just write and it’s live.
The total setup time? About 15 minutes. The time saved per post? At least 10 minutes of friction. Plus, the quality of life improvement is massive.
What’s your blogging setup? Would love to hear about different approaches. Drop a comment below or reach out on socials.
Thanks for reading! Want to see more content like this? Subscribe to the RSS feed or follow me on social media.

