Why This Stack?
Before diving into the how, let’s address the why. There are countless ways to publish onlineβMedium, Substack, WordPress, Ghost, Notion. Why choose this particular combination of tools?
Obsidian gives you the best writing experience. It’s local-first, Markdown-native, blazingly fast, and your content lives as plain files you actually own. No vendor lock-in. No subscription required for basic use. Your thoughts remain yours.
Hugo is the fastest static site generator. Written in Go, it builds thousands of pages in seconds. No JavaScript runtime, no complex build pipelinesβjust raw speed. The result is pure HTML/CSS that loads instantly.
Cloudflare Pages offers free hosting with a global CDN, automatic deployments from Git, and seamless custom domain support with free SSL. The free tier is generous enough for most personal sites.
Together, these three create a writing workflow that is:
- Fast: Sub-second builds, sub-100ms page loads
- Free: $0/month for hosting and deployment
- Portable: Plain Markdown files, no proprietary formats
- Resilient: Static files can be hosted anywhere if you ever need to migrate
Let’s build it.
Part 1: Setting Up Hugo Locally
Installing Hugo
Hugo requires a single binaryβno dependencies, no runtime.
macOS (Homebrew):
| |
Windows (Chocolatey):
| |
Linux (apt):
| |
Verify the installation:
| |
You should see something like hugo v0.123.0+extended. The extended version is importantβit includes SCSS/SASS support that many themes require.
Creating Your Site
| |
This creates the Hugo directory structure:
| |
Adding a Theme
Hugo has hundreds of themes. For this guide, I’ll use PaperModβclean, fast, and well-maintained.
| |
Update hugo.toml:
| |
Creating Your First Post
| |
This creates a file in content/posts/my-first-post.md:
| |
Change draft: true to draft: false when ready to publish.
Running Locally
| |
The -D flag includes draft posts. Open http://localhost:1313 to see your site. Hugo’s live reload means changes appear instantly.
Part 2: Setting Up Obsidian as Your Editor
Why Obsidian for Hugo?
Obsidian is a Markdown editor, and Hugo uses Markdown for content. They’re natural partners. But the real power comes from:
- Vault as Content Folder: Point Obsidian directly at your Hugo content directory
- Templates: Create consistent front matter with Obsidian templates
- Backlinks: See connections between your posts
- Graph View: Visualize your content’s structure
- Local & Fast: No cloud sync lag while writing
Configuring Obsidian
Open your Hugo content folder as a vault:
- Open Obsidian
- Click “Open folder as vault”
- Select your
my-blog/contentfolder
Create a templates folder:
1 2 3content/ βββ templates/ βββ blog-post.mdCreate a post template (
templates/blog-post.md):
| |
Configure the Templates plugin:
- Settings β Core plugins β Enable “Templates”
- Settings β Templates β Set template folder to
templates - Set date format to
YYYY-MM-DD
Set up a hotkey:
- Settings β Hotkeys β Search “Insert template”
- Assign something like
Cmd+Shift+T
Now when creating a new post, you can hit your hotkey and insert consistent front matter instantly.
Organizing Content
Structure your content folder to match Hugo’s expectations:
| |
Important: Add your templates folder to .gitignore so they don’t publish:
| |
Handling Images
Obsidian can paste images directly, but Hugo needs them in specific locations.
Option 1: Static folder
Store images in static/images/ and reference them:
| |
Option 2: Page bundles
Create a folder for your post with an index.md:
| |
Reference images relatively:
| |
Obsidian setting for images:
- Settings β Files & Links β Default location for new attachments
- Choose “In subfolder under current folder”
- Subfolder name:
images
Part 3: Git Setup and GitHub Repository
Initialize Git (if not already done)
| |
Create .gitignore
| |
Create GitHub Repository
- Go to github.com/new
- Create a new repository (e.g.,
my-blog) - Don’t initialize with README (you already have files)
Push to GitHub
| |
Part 4: Deploying to Cloudflare Pages
Why Cloudflare Pages?
- Free tier: 500 builds/month, unlimited bandwidth
- Global CDN: Your site is cached at 300+ edge locations
- Automatic deployments: Push to Git, site updates
- Preview deployments: Every branch gets a preview URL
- Built-in analytics: Privacy-respecting, free
Setting Up Cloudflare Pages
Create Cloudflare account (if needed): Go to dash.cloudflare.com and sign up
Navigate to Pages:
- Dashboard β Workers & Pages β Create application β Pages β Connect to Git
Connect GitHub:
- Authorize Cloudflare to access your GitHub
- Select your
my-blogrepository
Configure build settings:
Setting Value Production branch mainBuild command hugo --minifyBuild output directory publicRoot directory /(leave empty)Set environment variables:
Variable Value HUGO_VERSION0.123.0(or your version)This ensures Cloudflare uses a recent Hugo version.
Deploy: Click “Save and Deploy”
Cloudflare will clone your repo, run Hugo, and deploy the public folder. You’ll get a URL like my-blog-abc.pages.dev.
Understanding the Build Process
Every time you push to main:
- Cloudflare detects the push via webhook
- Spins up a build environment
- Clones your repository
- Runs
hugo --minify - Deploys the
public/folder to the CDN - Invalidates the cache globally
Build times are typically 10-30 seconds for most sites.
Part 5: Custom Domain Setup
Buying a Domain
If you don’t have a domain, you can purchase one from:
- Cloudflare Registrar: At-cost pricing, no markup
- Namecheap: Affordable, good UI
- Porkbun: Often cheapest for common TLDs
- Google Domains (now Squarespace): Clean interface
For this guide, I’ll assume you already have a domain.
Option A: Using Cloudflare as DNS (Recommended)
If you transfer your domain’s DNS to Cloudflare, setup is seamless.
Step 1: Add your domain to Cloudflare
- Dashboard β Add a Site β Enter your domain
- Select the Free plan
- Cloudflare scans existing DNS records
- You’ll receive two nameservers (e.g.,
adam.ns.cloudflare.com)
Step 2: Update nameservers at your registrar
- Go to your domain registrar
- Find DNS/Nameserver settings
- Replace existing nameservers with Cloudflare’s
- Wait for propagation (minutes to 48 hours)
Step 3: Add custom domain to Cloudflare Pages
- Go to your Pages project
- Custom domains β Add custom domain
- Enter
yourdomain.com - Cloudflare automatically creates the DNS record
- Repeat for
www.yourdomain.comif desired
Step 4: Configure redirects (optional)
To redirect www to apex (or vice versa), create a redirect rule:
- Dashboard β your domain β Rules β Redirect Rules
- Create rule:
- If: Hostname equals
www.yourdomain.com - Then: Dynamic redirect to
https://yourdomain.com${http.request.uri.path} - Status: 301 (permanent)
- If: Hostname equals
Option B: Using External DNS
If you want to keep DNS with another provider:
Step 1: Add CNAME record
At your DNS provider, add:
| |
Note: Some DNS providers don’t allow CNAME at the apex (@). Use ALIAS/ANAME if available, or consider moving DNS to Cloudflare.
Step 2: Add domain in Cloudflare Pages
- Custom domains β Add custom domain
- Enter your domain
- Cloudflare will verify via DNS lookup
SSL/TLS Configuration
Cloudflare automatically provisions SSL certificates for your custom domain. Ensure your SSL mode is correct:
- Dashboard β your domain β SSL/TLS β Overview
- Set encryption mode to Full (strict)
This ensures:
- HTTPS between visitors and Cloudflare (edge)
- HTTPS between Cloudflare and your origin (Pages)
Verifying Setup
After DNS propagates, verify:
| |
Part 6: The Complete Workflow
With everything configured, your publishing workflow becomes:
Daily Writing (in Obsidian)
- Open Obsidian (your content vault)
- Create new file in
posts/ - Insert template with your hotkey
- Write in distraction-free Markdown
- Add images to
static/images/ - Set
draft: falsewhen ready
Publishing
| |
That’s it. Cloudflare detects the push, builds, and deploys. Your post is live globally in under a minute.
Preview Before Publishing
Keep draft: true and push. Cloudflare builds it, but Hugo excludes drafts from production. To preview drafts:
- Create a
previewbranch - Configure Cloudflare Pages build command for non-production branches:
1hugo --minify --buildDrafts - Push to
previewbranch - Access via
preview.my-blog.pages.dev
Part 7: Performance Optimization
Your site is already fast, but let’s make it faster.
Hugo Build Optimization
In hugo.toml:
| |
Cloudflare Caching
Cache Rules: Dashboard β Caching β Cache Rules
- Cache everything for static assets
- Set browser TTL to 1 year for versioned assets
Speed β Optimization:
- Enable Auto Minify (HTML, CSS, JS)
- Enable Brotli compression
- Enable Early Hints
- Enable HTTP/3
Polish (Pro plan): Automatically optimizes images
Performance Headers
Create static/_headers:
| |
Lighthouse Score
With this setup, you should achieve:
- Performance: 95-100
- Accessibility: 95-100 (theme dependent)
- Best Practices: 100
- SEO: 100
Part 8: Troubleshooting Common Issues
“Page not found” after deployment
Cause: baseURL mismatch
Fix: Ensure hugo.toml has correct baseURL:
| |
Styles/CSS not loading
Cause: Mixed content or wrong baseURL Fix:
- Check baseURL includes
https:// - Verify theme is properly installed as submodule
Build fails on Cloudflare
Cause: Hugo version mismatch
Fix: Set HUGO_VERSION environment variable to match your local version:
| |
Images not showing
Cause: Incorrect paths Fix:
- For
static/images/foo.png, use/images/foo.png - For page bundles, use relative paths:
./image.png
Submodule not cloning
Cause: Theme submodule not initialized
Fix: Ensure .gitmodules exists and is committed:
| |
Conclusion
You now have a publishing system that:
- Costs nothing to host
- Loads instantly from global edge servers
- Deploys automatically when you push
- Stores content as portable Markdown files
- Writes beautifully in Obsidian’s distraction-free editor
The best part? This setup scales. Whether you have 10 posts or 10,000, the workflow remains the same. Hugo builds in seconds. Cloudflare serves globally. Your words reach readers at the speed of light.
Write locally. Think globally. Publish freely.
Quick Reference
| Task | Command/Action |
|---|---|
| New post | hugo new posts/slug.md or create in Obsidian |
| Local preview | hugo server -D |
| Build production | hugo --minify |
| Deploy | git push origin main |
| Check build status | Cloudflare Dashboard β Pages β Deployments |
Useful Links: