architecture straightshot go podman terraform containers deployment scatterarrow

I Built My Own Blog. Here's How.

In my previous article, I talked about the experience of building this blog with AI assistance. Now let me show you in more detail what actually got built.

I think it's quite possible I'll open source most of this stuff at some point. I just have to work on separating my personal data from the code first.

At the Root

This isn't a typical WordPress blog or even a standard static site generator setup. Instead, it's a modular system where each component has a single responsibility and can be developed independently:

scatterarrow-dotcom/
├── .github/                 # GitHub configuration and AI instructions
│   ├── instructions/        # Coding instructions for different file types
│   └── copilot-instructions.md
├── site/                    # HTML generation with straightshot
│   ├── content/
│   │   ├── about.md
│   │   ├── cv_data.yaml
│   │   ├── news/            # News items in markdown
│   │   ├── drafts/          # Work in progress articles in markdown
│   │   └── publish/         # Published blog posts in markdown
│   ├── templates/           # Jinja2 templates
│   ├── static/              # Static assets (icons, images)
│   ├── pyproject.toml       # Python dependencies and build config
│   ├── site.yaml            # Site configuration
│   ├── README.md
│   └── Taskfile.yml
├── frontend/                # JavaScript/CSS processing with esbuild
│   ├── src/
│   │   ├── css/             # Stylesheets and theming
│   │   └── js/              # TypeScript modules
│   ├── esbuild.config.js    # Build configuration
│   ├── package.json         # Node.js dependencies
│   ├── tsconfig.json        # TypeScript configuration
│   ├── README.md
│   └── Taskfile.yml
├── backend/                 # Go server for API and file serving
│   ├── handlers/            # REST API endpoints
│   ├── models/              # Data structures
│   ├── config/              # Configuration management
│   ├── go.mod go.sum        # Go stuff
│   ├── main.go
│   ├── README.md
│   └── Taskfile.yml
├── container/               # Container deployment configuration
│   ├── scripts/             # Like the health-check
│   ├── Containerfile
│   ├── podman-compose.yml
│   ├── README.md
│   └── Taskfile.yml
├── deploy/                  # Production deployment to Hetzner Cloud
│   ├── host-files/          # Server configuration files uploaded as is
│   ├── platforms/
│   │   └── hetzner/         # I'm preparing for other platforms
│   │       ├── cloud-init.yml
│   │       └── terraform/   # Infrastructure as code
│   ├── README.md
│   └── Taskfile.yml
├── docs/                    # Design documentation
├── _output/                 # Build artifacts and generated site
├── README.md                # Main project documentation
└── Taskfile.yml             # Root orchestration

Each module has its own Taskfile.yml and can be built, tested, and even run independently. The root orchestrates everything with commands like task setup or task build.

HTML Generation: straightshot

The content part uses straightshot - a Python-based static site generator that I created. It's not trying to compete with Hugo or Jekyll; it's deliberately simple and focused on what I need.

What straightshot does:

  • Processes Markdown files with frontmatter
  • Renders all HTML from Jinja2 templates with full Python power
  • Generates RSS feeds and sitemaps automatically
  • Handles syntax highlighting with Pygments
  • Supports custom template tags for YouTube video embedding, Google Slides and more.
  • Creates topic/article mappings, articles associations, and permalinks
  • Support for language switching when articles are available in more than one language.
  • Some SEO optimization, but no cookies, tracking or external analytics
  • Draft and published content management.

The content files live in site/content/ as Markdown files, get processed by templates in site/templates/, and output HTML to the local build output folder. The configuration in site.yaml controls exactly what gets processed, provides site metadata, and allows using any YAML and JSON data source inside templates. As an extreme example, I'm generating my CV fully from a YAML file - including layout order and placement of the individual sections. I even implemented different layouts for desktop, mobile and print (PDF) rendering. No need to use an office suite anymore!

All in all, it's nothing revolutionary, but it works exactly how I want it to. I can now write blog posts in Markdown, update my CV in YAML and have it available for the world in under a minute.

Frontend: No Framework Of The Day

The frontend module processes TypeScript and CSS through esbuild. No React, no Vue, no framework of the week. Just vanilla TypeScript that compiles to clean JavaScript modules. I might employ something like Tailwind at some point though, because while I felt the need to get my hands dirty with CSS, I still don't like fiddling around with it very much. I think the CSS is probably the worst part of the entire site. Please don't look at it. I know it can be done better; I just don't bother with it currently.

I got:

  • Dark/Light mode. Respects system preference, remembers your choice.
  • Copy buttons for source. Convenient!
  • Article language switchers (I'm not sure why I thoughy I need it. I wanted to have it nonetheless)
  • Social media share buttons (without embeedding any third-party crap though)
  • A blog post comment system. You can make comments on any blog posts without providing personal data or session cookies.

The frontend build writes to the build output folder - where the backend can serve it. CSS is processed but not overly complicated - just modern features like CSS custom properties for theming.

Backend: Just Use Go

Most static sites are, well, static. But I wanted people to be able to comment on this blog and have some basic analytics without depending on external services. So I needed a backend after all. I'm currently starting to use Go at work and Go seems like a very common language to use for web services. This is the perfect excuse for me to use it. Although I have to admit that there are a bunch of things that I like about Go, there are also a bunch of things I don't. More on that another time.

The Go backend (backend/) serves the generated site from a configurable location with proper caching headers and content types. I think I'm not using any frameworks other than the standard library here either. Maybe someday.

There is a REST API for the blog post comment system. Comments are stored as JSON files (no database needed), and there's a custom captcha system that asks simple questions instead of showing squiggly text.

Comment features:

  • Rate limiting (currently 5 seconds between actions, 50 per day per IP)
  • Optional user secrets for comment management (you can delete your own comments if you remember them)
  • Admin mode for moderation when someone gets nasty
  • File-based storage for simplicity
  • Approval system for comments for when things get really bad (deactivated currently)

Captcha system: Instead of "click all the traffic lights," users get questions like:

  • "How many letters are in the word 'captcha'?" (Answer: 7)
  • "What is the capital of France?" (Answer: Paris)
  • "What is 5+3?" (Answer: 8)
  • "What color is the sky on a clear day?" (Answer: blue)

I currently only have small catalogue of questions which are stored on the server and served randomly. I just hope it's unusual and effective enough to deter most bots.

Analytics

Basic visitor tracking without the privacy nightmare of Google Analytics:

  • Page views by URL
  • Session-based counting (not tracking users across visits)
  • Stored as JSON files

No cookies, no tracking pixels, no external requests to analytics services.

Containerization: Just for Fun

I actually didn't need to use containerization, I think. But I wanted to because I wanted to explore Podman as a technology. I kind of knew how to use Docker before, and now I know Podman as well.

The container setup is straightforward. A single Containerfile that:

  1. Starts with Alpine Linux (small, secure)
  2. Copies the pre-built Go binary
  3. Copies the full generated website
  4. Sets up a non-root user
  5. Exposes port 8080
  6. Runs a custom service health check every minute.

No multi-stage builds, no complex orchestration. The build artifacts from other modules get copied into a simple container that just works.

The podman-compose.yml supports both development and production through environment variables - same configuration, different settings.

I can run the container locally to test everything before I deploy it publicly with:

task container:run

Deployment: Infrastructure as Code

Deployment to Hetzner Cloud is handled through Terraform and cloud-init.

I'm using Terraform to provision a static IP, SSH key, a firewall, a volume and a VPS. I did some trickery around having a static deployment which I can actually import or destroy even if I lost my Terraform state file.

And cloud-init handles package installation (podman, nginx, certbot), user creation, firewall setup, basic system hardening, and a systemd service to keep the podman container running.

Afterwards, I upload my pre-built container image, request and install an SSL certificate from Let's Encrypt, and reconfigure nginx for HTTPS as it handles TLS termination and start the scatterarrow systemd service.

The deployment process overall looks like this:

task build
task deploy

That's it. I think it takes about 2 minutes from scratch at most. If I just need to update some content or the container, it's less than 30 seconds.

I have a bunch of utility commands to SSH into the host or the container, pull logs and other info from it and of course tear everything down as well.

Development Workflow

The modular structure makes development pleasant and fast (I think):

git clone <repo name>

# One-time setup things like downloading and installing all kinds of dependencies
task setup

# Build everything clean from scratch (including tests and static analysis etc)
task build
# or do this individually
task site:build      # Just the site generation
task frontend:build  # Just the JS/CSS
task backend:build   # Just the Go backend
task container:build # Just make a new container image

task serve           # Serve site locally with backend

# Container development
task container:run   # Run containerized environment locally, on a different port (just HTTP)

# Deploy to production
task deploy
# Check if everything is as it should be!
task status

Each module has its own directory and dependencies and can be worked on independently.

What I Like About It

The separation of concerns is probably what is most important to me. Content creation happens in Markdown and YAML, frontend interactions are handled with TypeScript and CSS, the backend API is written in Go, and deployment is managed through Terraform and containers. Each module has its own responsibilities and I'm not mixing programming languages where they don't belong, with clear boundaries and no weird two-way dependencies or anything like that. I guess if you are like me and have seen too many horrible codebases violating these ideas in many creative and horrible ways, you too understand why this is important.

Deployment is simple compared to some of the over-engineered systems I've worked with. The entire build process creates a single container with almost everything needed to run the site. After I deploy it, I just run a few scripts to get HTTPS certificates installed and nginx configured. No complex orchestration, no mysterious deployment pipelines that break when someone sneezes. It either works or it doesn't, and when it doesn't, it's quite obvious where to look.

I'm also quite happy that I don't depend on any external services for core functionality. Comments, analytics, and even the captcha system are all self-contained within my infrastructure. No vendor lock-in, no external API dependencies that could disappear tomorrow, no third-party services tracking my visitors. If some big company decides to shut down some service I was using, it likely doesn't affect me much if at all. Though I guess I'll have to eventually rely on a few more third-party services if this site grows.

The complexity feels maintainable because each part is simple enough to understand completely, but together they create a capable system. I can hold the entire architecture in my head, which means I can actually maintain it long-term without constantly rediscovering how things work. At least this is what I tell myself before I go to sleep.

And the site is fast and small! I just checked a first time visit of this particular blog post page is about 240 kB size and 32 kB transferred, and 220 ms total rendering time. A second visit with a cache (mostly for the SVGs of the social media sharing feature) reduced this to about 60 kB size and 12 kB transferred, and 60 ms total rendering time. This is rather unusual at a time when every site you visit has a couple of megabytes and takes like 3 seconds to render.

And I already have ideas to tweak that even further.

Future Tasks

Comments and analytics are just JSON files. This works fine at my scale but won't scale if the whole world decides to visit my site. I don't know yet what I'll do when that happens. (notice I didn't say "if", I said "when" :D).

There is no redundancy, no geographic distribution. If the server goes down, the site goes down. I also don't think I need to worry about that much yet either.

While a lot is automated, deployment still requires running commands manually. I have no tests, no CI/CD pipeline. I'm not even sure if I need it for a project like this. I certainly don't miss it yet. But I'm looking into GitHub Actions soon though.

I'm also thinking of bringing the HTML/content generation and CSS/JavaScript build closer together as they very much depend on each other. As of now, the "Frontend" treats the HTML as static while the HTML generation believes there is no CSS and JS at all. It works, but it is a bit iffy.

What I Learned

I guess I'm a full stack developer now. Am I? I never considered myself to be one. Actually I still don't. But I'm getting closer now.

I could explore a lot of the technology I'm using here much more. But for now, I'm just happy that I got to use so much stuff that I previously had only seen from a distance.

I think the comment system is interesting. Has anyone ever seen anything like it? Tell me what you think. I hope it's somewhat novel.

For a personal blog of a developer like me, it's exactly the right amount of engineering in my mind.

Comments

Loading security question...
This name is not protected - only the secret is
Also makes your name unique
No comments yet. Be the first to comment!