8 min read

From Notion to Hugo: Building a Fully-Automated Blog Pipeline

How I automated publishing from Notion to Hugo with custom themes, metadata, image optimization, and CI/CD for zero-click deployment

For years, I treated Notion as my private vault of ideas β€” essays, notes, and half-finished thoughts piled up in pages only I could access.

But I always wanted a public blog: something fast, professional, and structured, where I could share my writing with others. Not just a single-page notebook, but a real publishing system with categories, tags, SEO, search, author profiles, images, and automation.

The challenge was clear: how do I bridge the gap between a private Notion workspace and a fully functional blog β€” without manually copying and pasting every post?

⚑ The First Breakthrough: Hugo

The first decision was to adopt Hugo, a static site generator known for its speed and flexibility.

Why Hugo?

  • It can build thousands of pages in seconds.
  • It works Markdown-first, which pairs perfectly with Notion’s export format.
  • It supports themes, layouts, and shortcodes for maximum customization.
  • It generates static HTML files, which means the blog loads instantly and is easy to host anywhere.

With Hugo as the core, I now had a direction:

Notion β†’ Markdown β†’ Hugo β†’ Live Blog

But making this flow smooth required solving a number of challenges.

πŸ› οΈ The Challenges

At first glance, exporting from Notion to Hugo seemed simple. But the details made it complex:

  • Broken Exports β†’ Notion markdown often comes with broken image paths, missing frontmatter, and inconsistent formatting.
  • File Structure β†’ Hugo requires posts, images, and data files to live in well-structured folders.
  • Metadata β†’ Posts need titles, tags, categories, authors, and dates. Notion exports don’t provide these in Hugo-compatible format.
  • Themes & Styling β†’ Default Hugo themes didn’t fit my vision. I wanted to design my own.
  • Performance β†’ Optimizing images, scripts, and layouts for fast loading was critical.
  • Deployment β†’ I didn’t want to manually upload files every time. Continuous deployment was a must.

πŸ”‘ Building the System

Instead of giving up on Notion exports, I decided to engineer a full pipeline that could process them and produce a production-ready Hugo site.

Here’s what I built:

1. Processing Notion Files

I wrote scripts to process raw Notion exports:

  • Convert .md files into Hugo-compatible Markdown.
  • Inject frontmatter with title, tags, categories, and author information.
  • Move exported images into Hugo’s static/images/ folder and fix all references.
  • Normalize formatting (headings, code blocks, lists).

This turned messy Notion exports into clean, blog-ready Markdown files.

2. Theme Development

Rather than using an existing theme, I built a custom Hugo theme tailored for my blog:

  • Responsive design optimized for reading.
  • SEO tags (Open Graph, Twitter cards, sitemaps).
  • Author profiles with avatars and social links.
  • Support for tags, categories, and related posts.
  • Newsletter and contact forms integrated directly into layouts.

By building the theme myself, I could ensure it aligned perfectly with the automation pipeline.

3. Metadata & Organization

Each post automatically receives structured metadata. Example frontmatter:

---
title: "Building a Notion-to-Hugo Blog"
date: 2025-09-01
author: "Akshat Jain"
tags: ["notion", "automation", "hugo", "static-site"]
categories: ["blog-automation"]
description: "How I automated publishing from Notion to Hugo with custom themes, metadata, and CI/CD."
draft: false
---

I also organized everything into folders:

content/posts/      β†’ blog posts
static/images/      β†’ optimized images
data/authors/       β†’ author profiles
themes/mytheme/     β†’ custom Hugo theme
layouts/            β†’ templates

4. Image Optimization

Images were one of the biggest performance bottlenecks. I added an image processing step to:

  • Resize large Notion images.
  • Convert them to WebP for modern browsers.
  • Apply lazy loading so pages load instantly.

This ensured the blog remained lightweight and fast, even with many media-heavy posts.

import os
from pathlib import Path
from PIL import Image

# Base directory of your project (two levels up from this script)
BASE_DIR = Path(__file__).resolve().parent.parent

# Configuration with absolute paths
INPUT_DIR = BASE_DIR / "output" / "images"
WEBP_QUALITY = 85
SKIP_IF_LARGER = True

def get_kb(path):
    return os.path.getsize(path) / 1024

def crop_center_square(img):
    w, h = img.size
    min_dim = min(w, h)
    left = (w - min_dim) // 2
    top = (h - min_dim) // 2
    return img.crop((left, top, left + min_dim, top + min_dim))

def crop_center_16_9(img):
    w, h = img.size
    target_ratio = 16 / 9
    current_ratio = w / h

    if current_ratio > target_ratio:
        new_width = int(h * target_ratio)
        left = (w - new_width) // 2
        return img.crop((left, 0, left + new_width, h))
    else:
        new_height = int(w / target_ratio)
        top = (h - new_height) // 2
        return img.crop((0, top, w, top + new_height))

def process_image(file_path):
    filename = os.path.basename(file_path)
    name, ext = os.path.splitext(filename)
    ext = ext.lower()

    if ext not in [".jpg", ".jpeg", ".png"]:
        return

    input_kb = get_kb(file_path)

    with Image.open(file_path) as img:
        img = img.convert("RGB")
        original_img = img

        # Crop square
        if "square" in name.lower():
            img = crop_center_square(img)

        # Crop 16:9
        elif "thumb" in name.lower():
            img = crop_center_16_9(img)

        # Save WebP version
        webp_path = os.path.splitext(file_path)[0] + ".webp"
        img.save(webp_path, "WEBP", quality=WEBP_QUALITY)
        webp_kb = get_kb(webp_path)

        # Check if WebP is larger
        if SKIP_IF_LARGER and webp_kb > input_kb:
            os.remove(webp_path)
            print(f"⚠️ Skipped: {filename} β†’ {os.path.basename(webp_path)} | WebP was larger ({webp_kb:.1f} KB > {input_kb:.1f} KB)")
        else:
            os.remove(file_path)
            report_change(filename, os.path.basename(webp_path), input_kb, webp_kb)

def report_change(original_name, new_name, original_kb, new_kb):
    change_kb = new_kb - original_kb
    percent_change = (change_kb / original_kb) * 100

    if percent_change > 0:
        print(f"⚠️ Increased: {original_name} -> {new_name} | {original_kb:.1f} KB β†’ {new_kb:.1f} KB (+{percent_change:.1f}%)")
    else:
        print(f"βœ… Reduced: {original_name} -> {new_name} | {original_kb:.1f} KB β†’ {new_kb:.1f} KB (-{abs(percent_change):.1f}%)")

# Process all images
def processImages():
    for root, dirs, files in os.walk(INPUT_DIR):
        for file in files:
            process_image(os.path.join(root, file))

5. Automation with CI/CD

The final piece of the puzzle was automation. I wanted the entire publishing workflow to be hands-off after writing in Notion.

Using GitHub Actions, I built a CI/CD pipeline that connects directly with GitHub Pages:

  • On every push to main, the workflow triggers automatically.
  • Hugo builds the site into the /public directory.
  • The built site is deployed directly to GitHub Pages, making changes live within seconds.

This means:

βœ… No manual uploads or FTP transfers.
βœ… Every commit = a live blog update.
βœ… Continuous publishing β€” I write in Notion, export, commit, and everything else is automated.

πŸ”Ž What Hugo Really Does

At its core, Hugo does one thing extremely well:

It takes Markdown and converts it into static HTML.

Example:

# My First Blog Post
Written in Notion β†’ Exported β†’ Processed β†’ Built with Hugo.

Becomes:

<h1>My First Blog Post</h1>
<p>Written in Notion β†’ Exported β†’ Processed β†’ Built with Hugo.</p>

The magic is in everything Hugo adds around that:

  • Layouts and themes for consistent styling.
  • Taxonomies for tags and categories.
  • Partial templates for modular components like headers, footers, and sidebars.
  • Asset pipelines for CSS and JS optimization.

All of this happens in milliseconds.

🌐 Static Site Generation (SSG)

Traditional CMS systems (like WordPress) generate pages on demand, querying databases and rendering templates each time a visitor loads a page.

In contrast, Static Site Generators (SSG) like Hugo work differently:

  • Pages are pre-built at compile time.
  • The site is just static HTML, CSS, and JS files.
  • Everything can be served directly from a CDN.

This gives:

  • ⚑ Instant load speeds (no backend queries).
  • πŸ›‘οΈ Security (no dynamic server to hack).
  • πŸ“ˆ Scalability (a static site can serve millions of requests with ease).
  • πŸ’° Low cost (free hosting on GitHub Pages, Vercel, or Netlify).

This is why Hugo is such a great fit for modern publishing.

🌍 The Final System

After weeks of iteration, I now have a fully automated publishing pipeline that turns messy Notion exports into a fast, production-ready blog:

  1. ✍️ Write in Notion.
  2. πŸ“¦ Export and process files into Hugo-ready Markdown.
  3. πŸ—‚οΈ Organize posts, images, and metadata into the right folders.
  4. 🎨 Apply a custom Hugo theme with SEO, categories, tags, and author profiles.
  5. πŸ–ΌοΈ Optimize images for the web (resizing, WebP, lazy loading).
  6. ⚑ Hugo builds the site in seconds.
  7. πŸ”„ GitHub Actions deploys automatically to GitHub Pages.
  8. 🌐 The blog loads fast, scales effortlessly, and stays always up-to-date.

In short:

πŸ’‘ Think β†’ ✍️ Write β†’ πŸ“¦ Export β†’ ⚑ Build β†’ πŸš€ Deploy

Everything else is handled by the pipeline.

πŸš€ Try It Yourself

πŸ”— GitHub Repo β€” Notion-to-Hugo Automation
πŸ”— Live Blog Demo

✨ Final Thoughts

This wasn’t just about building a blog β€” it was about creating a publishing system that eliminates friction between writing and publishing.

Key takeaways from the pipeline:

  • 🎨 Custom Hugo theme for design, performance, and SEO.
  • πŸ—‚οΈ Structured metadata & taxonomy for tags, categories, and author profiles.
  • πŸ–ΌοΈ Image optimization for lightweight, fast-loading pages.
  • ⚑ Hugo static site generation for near-instant builds.
  • πŸ”„ GitHub Actions + GitHub Pages for zero-click deployment.

What began as a pile of scattered Notion notes is now a streamlined publishing engine β€” optimized, automated, and future-proof.

And the best part? Publishing is so fast and effortless that the blog feels lighter than air.

Related Articles