Static Site Improvements: Scheduled Posts, Preview Builds, Robots Writing the CSS

Static Site Improvements: Scheduled Posts, Preview Builds, Robots Writing the CSS

Static Site Improvements: Scheduled Posts, Preview Builds, Robots Writing the CSS 13 min read
Static Site Improvements: Scheduled Posts, Preview Builds, Robots Writing the CSS

I’ve standardized design, upgraded components, added client-side search, and configured browser caching for this site over the last few weeks. I’ve also adopted a workflow that helps me capture ideas, test changes, and get posts published more easily.

I’m not a web developer, I’m a database nerd. I’m don’t want to be great at CSS. I got this all done with a personal Cursor subscription and an obsessive urge to tag and tidy all my old posts. AI tooling these days makes static websites easier than ever to set up, design, and improve.

If you’re open to AI tooling and want to create a website (or upgrade one you already have), a static site is a great bet.

🔥 Related posts: I've written about my static site journey before. Check out Moving From WordPress to an Azure Static Site with Hugo for my migration story, and Adding a Netlify Contact Form to a Hugo Static Site for details on moving to Netlify.

Recap of My Site Setup

KendraLittle.com is a static site built with Hugo, a static site generator. I write posts in Markdown, Hugo builds them into HTML files, and Netlify hosts and deploys the site. There’s no database, no server-side code, and no Content Management System, just files in a free GitHub repository and Netlify.

I currently pay for a Netlify subscription and a Cursor subscription. It’s a hobby for me, I want some fun things, so those costs are worth it to me. (Netlify has a good free tier, but occasionally I’m lucky enough to exceed its bandwidth costs. And I feel like I get good value for what I pay for.)

This setup works well for a technical blog. It’s fast, secure, and easy to maintain.

And I have never missed WordPress.

Component Upgrades

I upgraded the major frontend components to bring the site up to date:

  • Bootstrap: Upgraded from 3.3.7 to 5.3.3
  • Hugo: Upgraded from 0.124.1 to 0.152.2

The Hugo upgrade here was a big deal. There were some important fixes between those two versions, and I’d struggled to do this upgrade on my own before. I knew it was important to complete this before making more changes.

These upgrades required template changes. Bootstrap 5 changed class names (navbar-togglenavbar-toggler, data-toggledata-bs-toggle). Hugo 0.152.2 deprecated some template functions, so I updated those too. Cursor helped identify all the places that needed changes, which made the upgrade process manageable.

It wasn’t bad this time at all, especially with the help of…

Preview Deployments

Every time I open a pull request or push a change to a branch with a pull request, Netlify creates a preview deployment. I can see how the site looks and behaves before merging to main. This is especially valuable for CSS changes: I can test button heights, spacing, and mobile layouts in a real deployment without affecting the live site.

It’s basically a staging environment that’s automatically created for every pull request, and you can access it on different devices. (This post is not sponsored by Netlify, but this database person sure loves a staging site. They include this in the free plan, by the way.)

Local Development Workflow

I’m currently using Windows for my personal nerdery, so I have a PowerShell script that runs Hugo with specific flags for local development:

  • --disableFastRender to ensure CSS changes are visible immediately
  • --noHTTPCache to prevent browser caching issues
  • --buildFuture to preview future-dated posts

The script also syncs CSS files before starting the server and optionally builds the search index. These customizations make local development smoother: I can see CSS changes right away– at least usually. Admittedly, there are some times when I still have to do a full build of the site to see changes and I don’t fully understand why, but it’s not yet been a big enough problem to dive into.

Here’s the Hugo command the script runs:

hugo server --port 1313 --bind 127.0.0.1 --poll 700ms --disableFastRender --noHTTPCache --buildFuture

The --poll flag helps with reliable file watching on Windows, and --buildFuture lets me preview posts scheduled for future dates.

Standardized Buttons and Forms

Before, buttons and form fields had wildly inconsistent heights, padding, and styling. I had a hard time fixing it.

I used Cursor to find all button definitions and help standardize them. This cut down on the repetitive work, and chatting with the robot helped me figure out how to troubleshoot visual issues in Firefox somewhat efficiently (well, sometimes).

I standardized everything to a 38px height:

  • Buttons: All buttons (navigation, forms, sidebars) are 38px tall with consistent padding
  • Form fields: All input fields, selects, and textareas match the 38px height
  • Navigation links: Navbar links use the same 8px horizontal padding as buttons for visual consistency

It ain’t perfect, but it’s a lot better.

Mobile Improvements

I added mobile optimizations for screens under 768px:

  • Touch targets: Buttons and form fields meet the 44px minimum for easy tapping
  • Font sizes: Set to 16px minimum to prevent iOS auto-zoom on input focus
  • Navigation: Fixed navbar visibility and mobile menu behavior
  • Layout: Reduced padding and improved spacing for small screens
  • Typography: Scaled headings appropriately for mobile readability

These changes make the site easier to use on phones and tablets.

Client-Side Search with Pagefind

I added Pagefind for client-side search. It indexes the built HTML files and provides search entirely in the browser, no backend needed. Here’s my new search page, and there’s a search form in the footer, too.

How it works:

  • Pagefind indexes the site after Hugo builds
  • Search happens in the browser using the index files
  • Results are sorted by date (most recent first)

Why this works for static sites:

  • No server-side search needed
  • Fast search results (everything happens client-side)
  • Works with static site hosting (no backend required)

GitHub Issues for Blog Post Ideas and Bugs

I’m now using GitHub Issues to track bugs and planned blog posts.

The most important thing about this is that I can quickly create a GitHub issue on my phone for a blog post idea, wherever I am when I think about it. Or I can screenshot a bug on the site and create an issue for that, then forget it for the moment.

Years ago, when I was a part of Brent Ozar Unlimited, I had a big old list of blog post ideas in a shared GitHub repo with the team that I could pull from anytime I was in the mood to write. I loved it. It feels good to be getting back to that, even if it’s just me.

Automatic Issue Syncing

I created git hooks that automatically sync GitHub issues to local markdown files:

  • post-checkout hook: Syncs issues when you switch branches
  • post-merge hook: Syncs issues after pulling or merging

The hooks run a Python script that fetches issue data from the GitHub API and saves it to issues/issue-{number}.md. This lets me reference issues locally without opening GitHub in a browser.

I’m sure there are extensions and various other ways to do this, but I like markdown files, OK? My cursorrules file knows where my issues are, so I can also use prompts like, “new branch to write post in issue 29 to publish on Jan 10,” and it can get my workflow started and have an idea what basic things to put in the front matter of the new file it creates for me.

I may switch this to a different mechanism than a git hook. Git is complex enough. But it’s good enough for now.

🔥 Front matter is metadata at the top of a Markdown file, enclosed in --- markers. It contains information like the post title, date, categories, and tags. Hugo uses this metadata to generate the site structure and HTML.

Standardizing Categories and Tags

I spent a good chunk of time cleaning up categories and tags across all my old blog posts. I had inconsistent taxonomy: tons of posts used “sql” as a category, many posts were missing tags entirely.

What I standardized:

  • Categories: Established 9 main categories (like query-performance, indexing, database-administration) and moved old posts to the right categories. I also added azure-sql as a new category for cloud database content.
  • Tags: Cleaned up generic tags and replaced them with specific technical terms. Each post now has 3-10 meaningful tags that reflect what’s actually in the content. You can now find all my love letters to azure-sql-managed-instance all in one place.
  • Format: Standardized everything to lowercase with hyphens (e.g., query-store not Query Store or query_store).

This was not something that Cursor made super easy. I have 580+ posts at this point, and I didn’t feel like building a whole sentiment/content analysis machine. A few attempts to use python scripts to generate the tags didn’t go well, the tags were often ridiculous. Cursor doesn’t love going through that many posts manually, either.

I worked through a fair amount of this while cooking things for Thanksgiving and doing other things. Not letting perfect be the enemy of good helped get me through it, and I’m pretty happy with where we landed.

Future-Dated Posts for Scheduling

Hugo supports future-dated posts. I can set a post’s date in the future, and Hugo won’t publish it until that date arrives.

How it works:

  • Set the date in a post’s front matter to a future date.
  • Hugo won’t include the post in builds until after the build runs on a computer where it’s that date or later.
  • Netlify automatically rebuilds KendraLittle.com daily (more on that next), so future posts publish automatically.

Here’s an example of front matter for a scheduled post:

---
title: "My Future Post"
date: 2026-01-10
draft: false
---

This is a simple way to scheduling posts in advance. The static site generator (Hugo) handles the scheduling logic, and automated builds handle the publishing.

Netlify Automated Builds

Netlify automatically builds and deploys the site when I push to the main branch. It also runs scheduled builds twice daily.

How scheduled builds work:

  1. Netlify’s scheduled function (configured under netlify/functions/) triggers at set times using a cron schedule
  2. The function calls a Netlify build hook URL, which triggers a new build
  3. Hugo builds the static site (including all posts with the date set to the current date or prior)
  4. Pagefind indexes the site for search
  5. Netlify deploys the result

Setting up the build trigger: I created a Netlify scheduled function that uses the @netlify/functions package’s schedule helper. The function is configured with a cron expression in netlify.toml and calls a build hook URL that Netlify generates. When you POST to that URL, it triggers a new deployment.

Here’s the scheduled function code:

import fetch from 'node-fetch'
import { schedule } from '@netlify/functions'

const BUILD_HOOK = 'https://api.netlify.com/build_hooks/YOUR_HOOK_ID'

// Runs twice daily: 11 AM UTC (6 AM EST) and 1 AM UTC (8 PM EST)
const handler = schedule('0 11,1 * * *', async () => {
  const response = await fetch(BUILD_HOOK, {
    method: 'POST'
  })
  
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Deploy triggered successfully' })
  }
})

export { handler }

The cron expression '0 11,1 * * *' runs at 11 AM UTC and 1 AM UTC daily. You configure this in netlify.toml:

[functions.scheduled-deploy]
  schedule = "0 11,1 * * *"

Cache Headers

I’ve configured cache headers in netlify.toml to improve performance. Static assets (CSS, JavaScript, images, fonts) are cached for 1 year with the immutable flag: these files have versioned filenames from Hugo’s asset pipeline, so aggressive caching is safe. HTML files are cached for 1 hour with must-revalidate, which balances performance with the need to show updated content.

Here’s the cache header configuration in netlify.toml:

# Static assets - cache for 1 year (with versioning in filename)
[[headers]]
  for = "/*.css"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.js"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/images/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

# HTML files - cache for 1 hour with revalidation
[[headers]]
  for = "/*.html"
  [headers.values]
    Cache-Control = "public, max-age=3600, must-revalidate"

The site also uses a service worker for offline support and client-side caching. Initially, it was using “Cache First” for images even during local development, which meant updated images wouldn’t show up in preview until I cleared the browser cache. I updated the service worker to detect when it’s running on localhost and use “Network First” for images in development, so image updates appear immediately. In production, it still uses “Cache First” for better performance.

Want to Build Your Own?

Here’s what I’m currently using and how to get started:

Core components:

Getting started:

  1. Create a new GitHub repository for your site
  2. Install Hugo: brew install hugo (Mac) or choco install hugo (Windows) or download from hugo.io
  3. Clone your repo and create a new site: git clone <your-repo-url> && cd <repo-name> && hugo new site .
  4. Create a .gitignore file with public/ and .hugo_build.lock (Hugo generates these, don’t commit them)
  5. Choose a theme: Browse Hugo themes. To use the Universal theme, I add it as a git submodule: git submodule add https://github.com/devcows/hugo-universal-theme themes/universal
  6. Configure config.toml with theme = "universal" (or your chosen theme name)
  7. Create your first post: hugo new blog/my-first-post.md
  8. Run locally: hugo server (view at http://localhost:1313)
  9. Commit and push: git add . && git commit -m "Initial Hugo site" && git push
  10. Deploy: Choose a hosting option (all have free tiers):
  • Netlify: Connect the repo in Netlify (includes preview deployments for pull requests)
  • GitHub Pages: Enable Pages in your repo settings and configure GitHub Actions to build and deploy
  • Cloudflare Pages: Free hosting with global CDN and automatic deployments

The Hugo documentation is excellent, and the Universal theme has good examples to get you started. Add Pagefind for search after you have content, and customize from there.

What About Jekyll?

Jekyll is another popular static site generator, and it’s the default for GitHub Pages. The main difference is that Jekyll is Ruby-based while Hugo is Go-based. Jekyll has a larger plugin ecosystem, while Hugo uses Go templates. Jekyll has big fans and a huge community. I’ve experimented with it recently on a new site and I really like it.

For now, I’m resisting the urge to give this site a Jekyll makeover, but if it still seems like a fun idea in a couple of months I may give it a shot.