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):

1
brew install hugo

Windows (Chocolatey):

1
choco install hugo-extended

Linux (apt):

1
sudo apt install hugo

Verify the installation:

1
hugo version

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

1
2
hugo new site my-blog
cd my-blog

This creates the Hugo directory structure:

1
2
3
4
5
6
7
8
9
my-blog/
β”œβ”€β”€ archetypes/      # Templates for new content
β”œβ”€β”€ assets/          # Files processed by Hugo Pipes
β”œβ”€β”€ content/         # Your Markdown content lives here
β”œβ”€β”€ data/            # Configuration data files
β”œβ”€β”€ layouts/         # Custom template overrides
β”œβ”€β”€ static/          # Static files (images, etc.)
β”œβ”€β”€ themes/          # Hugo themes
└── hugo.toml        # Main configuration file

Adding a Theme

Hugo has hundreds of themes. For this guide, I’ll use PaperModβ€”clean, fast, and well-maintained.

1
2
git init
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

Update hugo.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
baseURL = "https://yourdomain.com/"
languageCode = "en-us"
title = "Your Blog Name"
theme = "PaperMod"

[params]
  author = "Your Name"
  description = "Your blog description"
  defaultTheme = "dark"
  ShowReadingTime = true
  ShowPostNavLinks = true
  ShowBreadCrumbs = true
  ShowCodeCopyButtons = true

[params.homeInfoParams]
  Title = "Welcome"
  Content = "Your welcome message here"

[[params.socialIcons]]
  name = "github"
  url = "https://github.com/yourusername"

[[params.socialIcons]]
  name = "twitter"
  url = "https://twitter.com/yourusername"

[menu]
  [[menu.main]]
    identifier = "posts"
    name = "Blog"
    url = "/posts/"
    weight = 10
  [[menu.main]]
    identifier = "tags"
    name = "Tags"
    url = "/tags/"
    weight = 20
  [[menu.main]]
    identifier = "about"
    name = "About"
    url = "/about/"
    weight = 30

Creating Your First Post

1
hugo new posts/my-first-post.md

This creates a file in content/posts/my-first-post.md:

1
2
3
4
5
6
7
---
title: "My First Post"
date: 2026-01-05
draft: true
---

Your content here...

Change draft: true to draft: false when ready to publish.

Running Locally

1
hugo server -D

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:

  1. Vault as Content Folder: Point Obsidian directly at your Hugo content directory
  2. Templates: Create consistent front matter with Obsidian templates
  3. Backlinks: See connections between your posts
  4. Graph View: Visualize your content’s structure
  5. Local & Fast: No cloud sync lag while writing

Configuring Obsidian

  1. Open your Hugo content folder as a vault:

    • Open Obsidian
    • Click “Open folder as vault”
    • Select your my-blog/content folder
  2. Create a templates folder:

    1
    2
    3
    
    content/
    └── templates/
        └── blog-post.md
    
  3. Create a post template (templates/blog-post.md):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
---
title: "{{title}}"
date: {{date}}
tags:
  - 
draft: true
description: 
---

## Introduction



## Main Content



## Conclusion
  1. Configure the Templates plugin:

    • Settings β†’ Core plugins β†’ Enable “Templates”
    • Settings β†’ Templates β†’ Set template folder to templates
    • Set date format to YYYY-MM-DD
  2. 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:

1
2
3
4
5
6
7
8
content/
β”œβ”€β”€ posts/           # Blog posts
β”‚   β”œβ”€β”€ _index.md    # Section page content
β”‚   └── my-post.md
β”œβ”€β”€ notes/           # Shorter notes (if using)
β”œβ”€β”€ projects/        # Project showcases
β”œβ”€β”€ about.md         # About page
└── templates/       # Obsidian templates (add to .gitignore)

Important: Add your templates folder to .gitignore so they don’t publish:

1
echo "content/templates/" >> .gitignore

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:

1
![Alt text](/images/my-image.png)

Option 2: Page bundles

Create a folder for your post with an index.md:

1
2
3
content/posts/my-post/
β”œβ”€β”€ index.md
└── featured.jpg

Reference images relatively:

1
![Alt text](featured.jpg)

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)

1
2
cd my-blog
git init

Create .gitignore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cat << 'EOF' > .gitignore
# Hugo
public/
resources/_gen/
.hugo_build.lock

# Obsidian
.obsidian/
content/templates/

# OS files
.DS_Store
Thumbs.db

# Editor
*.swp
*.swo
EOF

Create GitHub Repository

  1. Go to github.com/new
  2. Create a new repository (e.g., my-blog)
  3. Don’t initialize with README (you already have files)

Push to GitHub

1
2
3
4
5
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/yourusername/my-blog.git
git push -u origin main

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

  1. Create Cloudflare account (if needed): Go to dash.cloudflare.com and sign up

  2. Navigate to Pages:

    • Dashboard β†’ Workers & Pages β†’ Create application β†’ Pages β†’ Connect to Git
  3. Connect GitHub:

    • Authorize Cloudflare to access your GitHub
    • Select your my-blog repository
  4. Configure build settings:

    SettingValue
    Production branchmain
    Build commandhugo --minify
    Build output directorypublic
    Root directory/ (leave empty)
  5. Set environment variables:

    VariableValue
    HUGO_VERSION0.123.0 (or your version)

    This ensures Cloudflare uses a recent Hugo version.

  6. 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:

  1. Cloudflare detects the push via webhook
  2. Spins up a build environment
  3. Clones your repository
  4. Runs hugo --minify
  5. Deploys the public/ folder to the CDN
  6. 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.

If you transfer your domain’s DNS to Cloudflare, setup is seamless.

Step 1: Add your domain to Cloudflare

  1. Dashboard β†’ Add a Site β†’ Enter your domain
  2. Select the Free plan
  3. Cloudflare scans existing DNS records
  4. You’ll receive two nameservers (e.g., adam.ns.cloudflare.com)

Step 2: Update nameservers at your registrar

  1. Go to your domain registrar
  2. Find DNS/Nameserver settings
  3. Replace existing nameservers with Cloudflare’s
  4. Wait for propagation (minutes to 48 hours)

Step 3: Add custom domain to Cloudflare Pages

  1. Go to your Pages project
  2. Custom domains β†’ Add custom domain
  3. Enter yourdomain.com
  4. Cloudflare automatically creates the DNS record
  5. Repeat for www.yourdomain.com if desired

Step 4: Configure redirects (optional)

To redirect www to apex (or vice versa), create a redirect rule:

  1. Dashboard β†’ your domain β†’ Rules β†’ Redirect Rules
  2. Create rule:
    • If: Hostname equals www.yourdomain.com
    • Then: Dynamic redirect to https://yourdomain.com${http.request.uri.path}
    • Status: 301 (permanent)

Option B: Using External DNS

If you want to keep DNS with another provider:

Step 1: Add CNAME record

At your DNS provider, add:

1
2
3
4
Type: CNAME
Name: @ (or your subdomain)
Target: my-blog-abc.pages.dev
TTL: Auto

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

  1. Custom domains β†’ Add custom domain
  2. Enter your domain
  3. Cloudflare will verify via DNS lookup

SSL/TLS Configuration

Cloudflare automatically provisions SSL certificates for your custom domain. Ensure your SSL mode is correct:

  1. Dashboard β†’ your domain β†’ SSL/TLS β†’ Overview
  2. 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:

1
2
3
4
5
6
7
8
# Check DNS resolution
dig yourdomain.com

# Check HTTPS
curl -I https://yourdomain.com

# Check redirect (if configured)
curl -I https://www.yourdomain.com

Part 6: The Complete Workflow

With everything configured, your publishing workflow becomes:

Daily Writing (in Obsidian)

  1. Open Obsidian (your content vault)
  2. Create new file in posts/
  3. Insert template with your hotkey
  4. Write in distraction-free Markdown
  5. Add images to static/images/
  6. Set draft: false when ready

Publishing

1
2
3
4
# From your Hugo root directory
git add .
git commit -m "New post: Your Post Title"
git push

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:

  1. Create a preview branch
  2. Configure Cloudflare Pages build command for non-production branches:
    1
    
    hugo --minify --buildDrafts
    
  3. Push to preview branch
  4. 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[build]
  writeStats = true

[minify]
  disableXML = true
  minifyOutput = true

[outputs]
  home = ["HTML", "RSS", "JSON"]

[imaging]
  quality = 80
  resampleFilter = "Lanczos"

Cloudflare Caching

  1. Cache Rules: Dashboard β†’ Caching β†’ Cache Rules

    • Cache everything for static assets
    • Set browser TTL to 1 year for versioned assets
  2. Speed β†’ Optimization:

    • Enable Auto Minify (HTML, CSS, JS)
    • Enable Brotli compression
    • Enable Early Hints
    • Enable HTTP/3
  3. Polish (Pro plan): Automatically optimizes images

Performance Headers

Create static/_headers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block
  Referrer-Policy: strict-origin-when-cross-origin

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/*.css
  Cache-Control: public, max-age=31536000, immutable

/*.js
  Cache-Control: public, max-age=31536000, immutable

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:

1
baseURL = "https://yourdomain.com/"

Styles/CSS not loading

Cause: Mixed content or wrong baseURL Fix:

  1. Check baseURL includes https://
  2. 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:

1
hugo version  # Check 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:

1
2
3
git submodule add --force https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
git add .gitmodules themes/PaperMod
git commit -m "Add theme submodule"

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

TaskCommand/Action
New posthugo new posts/slug.md or create in Obsidian
Local previewhugo server -D
Build productionhugo --minify
Deploygit push origin main
Check build statusCloudflare Dashboard β†’ Pages β†’ Deployments

Useful Links: