A blog I don’t have to babysit

Astro builds it, GitHub Actions checks and ships it, GitHub Pages serves it, and Cloudflare handles DNS. A small blog, everything as code.

Not every post gets to involve a firewall quietly eating half an Internet connection. This one is less dramatic: it is about how I built the place where I write about those things.

It is not the most spectacular project on this blog. But I know Lechu, Maniek and Patryk will need something similar, so I might as well write it down once instead of explaining the same setup three times. This one is partly for you, gents.

I wanted a small, personal site. Comfortable to read. A little odd around the edges. Somewhere for enterprise-cloud problems, automation, things made from LEGO, and the occasional photograph that does not need an entire article.

I did not want to acquire another application to operate just so I could publish some paragraphs.

Four pieces. Four different jobs.

The site lives in this GitHub repository and is published at notes.ciechanowi.cz. The important distinction is that GitHub Pages is the host, not the program that understands my article templates.

Cloudflare comes first, at DNS. I put the domain there partly because it is useful, partly because I like having somewhere to do a little infrastructure magic: change things, add records, call an API, and see what else I can make it do. There is room to play with rules and other edge features later. In the setup published here, though, the hostname points straight to GitHub Pages: it is DNS-only, not an active Cloudflare HTTP proxy. Cloudflare Pages is a different product; it is not hosting this blog.

  • Cloudflare handles DNS. It gives me control over the domain’s records, with APIs for programmatic changes when I want to go beyond clicking around a dashboard.
  • Astro builds the site. It reads the content, applies my layouts, produces the article pages and lists, and processes the images I ask it to process.
  • GitHub Actions runs the publishing workflow. It installs the dependencies, checks the project, builds it and hands the resulting artifact to the deployment job.
  • GitHub Pages serves the result. Visitors receive HTML, CSS, JavaScript and images. They do not start an Astro application on a server every time they open an article.

The static output is a consequence of this site’s configuration, not a claim about everything Astro can do:

export default defineConfig({
  site: 'https://notes.ciechanowi.cz',
  output: 'static',
  trailingSlash: 'always',
});

The build writes the website into dist/. That is what gets published. The custom domain is configured for GitHub Pages, and the repository carries a public/CNAME file with the hostname. HTTPS is enforced on the published site.

There are obviously still servers. They are just not application servers I maintain for this blog. GitHub Pages is a static hosting service, and that happens to match what these notes need.

Everything as code

That is the bit I love. Changes become commits. I get version history, reviewable diffs and automation instead of a growing collection of steps to remember. The site, the content and the publishing workflow live together in the repository; Cloudflare’s API gives me a way to take the same approach beyond the site itself. Less clicking the same thing twice. More describing what I want and letting the machinery repeat it.

Why use a framework for something this small?

The first versions were plain HTML. That was useful: I could explore the typography, spacing, colours and illustrations without making a publishing-system decision first.

But a collection of prototypes is not a particularly good way to maintain a growing blog. Once there are several articles, I do not want to copy a header into every file, remember to update an index, or manually split an archive into pages. That is bookkeeping, not writing.

I chose Astro because it lets me keep the result close to ordinary HTML while taking over that bookkeeping. The components can render HTML at build time without bringing a client-side framework runtime along for the ride. That is how Astro’s component model works; I am not using React or Vue here.

This is not a declaration that every other static-site generator is wrong. I was not trying to win a framework tournament. I wanted reusable HTML-like layouts, structured Markdown content, local-image handling and static output. Astro put those things together in a way that fitted the site I had already designed.

The framework did not choose the design. There is no purchased theme hiding underneath it. The small heading, quiet separators, warm accent and marginal sketches are choices in my templates and CSS.

And “no framework runtime” does not mean “no JavaScript anywhere.” The lightbulb has a small script. It switches the theme and carries an explicit light-theme choice through internal links. An ordinary visit starts dark; an old saved preference does not silently overrule that. I do not need a component application to change some CSS variables.

A post is a folder, not a dashboard

The published articles live under src/content/posts/. A post can have its own directory, with the text and its local assets together:

src/content/posts/
  my-next-note/
    index.md
    photo.jpg

The Markdown starts with frontmatter. These are fields the site actually knows about, not a second document I have to maintain somewhere else:

---
title: A blog I don't have to babysit
description: A small static site, with a publishing workflow I can explain.
slug: a-blog-i-dont-have-to-babysit
pubDate: 2026-10-17
tags: [Web, Systems]
readingMinutes: 10
---

The date above is example editorial metadata, not a deployment instruction. The title, description and tags feed the page and its listing; the slug gives the article its URL. The reading-time value is explicit in this version of the site, not secretly calculated by Astro.

Content Collections loads the Markdown and applies the schema defined in src/content.config.ts. In this repository, that schema checks for a title, description, date, tags and a positive reading-time value. I would rather discover missing metadata while building than after publishing.

A local image in the body is straightforward:

![A useful view of the thing I am discussing](./photo.jpg)

Images stored in src/ can go through Astro’s processing pipeline. Files in public/ are copied as they are. I use that distinction deliberately: the pfSense speed-test screenshot lives in public/images/pfsense/, so the original PNG remains available behind the framed image in the article. Astro documents both paths.

Gallery of Things sorts photographs by width-to-height ratio, from portrait to landscape, so neighbouring images have similar heights in its two-column layout. Images with the same proportions retain their configured order; the photographs are not cropped to force a uniform shape.

A useful trap to avoid: this site does not currently have a draft: true publishing filter. Its collection loader includes the Markdown files in that directory. Keeping an unapproved draft outside the published collection is a workflow decision, not a feature I should assume exists.

That is why this post started as a local, standalone HTML review copy. I can read it, change the wording and judge the illustrations before it becomes a content entry. Writing a draft should not be the same action as publishing it.

The archive should not become an endless front page

The listing reads the collection, sorts by publication date, uses the entry ID to break ties, and calls paginate(posts, { pageSize: 10 }). The route is src/pages/[...page].astro, so the first page is / and subsequent pages are /2/, /3/ and so on.

More → is a normal link to the next generated page. There is a return link to newer notes, and no next link on the last page. No infinite scroll, no browser-side download of the entire archive so that most of it can then be hidden. Astro’s paginator supplies the page data and URLs.

I wanted the pictures to belong to this place

I spent an unreasonable amount of time on the part that is not a framework feature: the drawings.

I wanted an illustration style of my own. Not a collection of unrelated stock images, and not a different visual personality every time I published. Something that could sit next to a firewall investigation, a cloud-automation note or a LEGO contraption and still feel like the same notebook.

The direction I kept was sparse: slightly wandering lines, rounded ends, plenty of empty space, and a few restrained warm marks. More quick pen study than polished enterprise diagram. The drawing should introduce an idea without competing with the text.

For a less abstract example, look at the network drawing in the pfSense article. My actual setup, in the same loose lines and warm accent as the rest of the site. That is exactly what I wanted: a technical picture that explains something real and still looks like it belongs here. My setup, my visual language—not a vendor diagram dropped into the middle of a notebook.

These are SVG sketch studies created during the design process, not scans of a paper notebook. Here are some of the actual earlier examples, rather than a mood board pretending to be the work:

A cloud and a routerThe drawing used with the pfSense post: a small object in the margin, not a giant cover competing with the title.
A letterbox with a little curiosity An imperfect pen outline of a rounded letterbox, a lifted flag, an envelope and a tiny sensor.
The curious letterboxAn earlier Quiet Ledger study. This was one of the loose, understated directions I wanted to carry forward.
Small clouds, repeated work Three loosely outlined clouds, a little toothed wheel and a returning arrow suggest the quiet repetition of cloud automation.
Clouds and repeated workFrom the later layout exploration: the same visual vocabulary applied to automation. A design study, not a published article.
A camera made of small pieces A block-built camera with a six-bladed aperture, a small shutter button and two loose studded bricks, outlined like a quick pen study.
A camera made of small piecesA LEGO-shutter study from the same pass. Again, an illustration experiment—not a claim that the corresponding post already exists.

The illustration beside this article’s title follows those same rules: a notebook, a folded page, a wandering cable and a small cloud. It suggests the subject without trying to explain the whole deployment pipeline in clip art.

The theme matters here, too. The inline drawings borrow their ink and accent from the page’s CSS variables, so they can live in both palettes. The visual language is mine to keep consistent; Astro is just the machinery that lets me place it in the right template.

Even the full stop in Here be clouds. has a small job: it is a native link to the paragraph explaining the name. That is the scale of ornament I was after. A bit of curiosity, not an interface that wants applause.

GitHub Actions is the publishing assistant, not the host

I could run a build on my laptop and upload the output myself. The problem is not that this is impossible. It is that I would have to remember which checks I ran and whether the files I uploaded were actually built from the version I intended to publish.

The repository’s .github/workflows/deploy.yml makes that sequence explicit. It runs for pull requests, pushes to main, and manual workflow runs. The build job uses Node 24 and performs:

npm ci
npm run check
npm run build

npm ci installs the dependency versions recorded in the lockfile. npm run check invokes astro check. The build then generates the static pages and assets into dist/. The workflow uploads that directory as a Pages artifact, and the separate deployment job publishes the artifact.

Pull requests build and check the site, but do not deploy it. The deployment condition allows only a non-pull-request run on main. In practice, merging a PR publishes the change—and a direct push to main also publishes it. The workflow is not a branch-protection rule, and I am not going to pretend that every update is forced through a review gate.

There are a couple of small details I like: the action references are pinned to commits, the build starts with read-only repository permission, and the Pages write permission and identity-token permission are granted to the deployment job. None of that removes the need to maintain dependencies. It makes the intended trust boundary visible.

For my friends borrowing this pattern, there is an important setup step outside the YAML: GitHub Pages must use GitHub Actions as its publishing source. The site URL, custom-domain configuration and CNAME must refer to your domain, not mine. Astro’s GitHub Pages deployment guide covers that wiring.

A green build is necessary. It is not a design review.

I separate three questions that are easy to blur together.

Does the project check and build?

Those are the automated checks in the workflow. astro check runs diagnostics, including type checking in Astro files; astro build exercises the build-time routes, content and asset processing needed to produce the site. A missing required content field or an unresolved local asset is something I want to catch here.

Does the built site actually behave correctly?

For day-to-day editing I can use npm run dev. Before publishing, I also want to look at the production output, not just the development server:

npm run check
npm run build
npm run preview

The preview command serves the built site locally; it is not the production hosting solution. During this build, the generated files were also served by a plain local static HTTP server. That is a useful sanity check for a site that is supposed to consist of files.

The browser smoke pass covered wide and narrow layouts, dark and light, the lightbulb with mouse and keyboard, theme preservation between pages, full-size image links and the full-stop link into About. For the archive, I also exercised the first-page boundary, the overflow page and its return link with temporary entries. Those entries were removed; they are not fake articles padding the archive.

The visual checks matter. An image can exist in the output and still be badly framed. A page can build and still overflow a phone screen. A fragment can be valid HTML and still land somewhere unhelpful.

What this setup does not claim: those browser checks are a separate smoke pass, not an end-to-end test suite currently run by GitHub Actions. The workflow runs check and build. It does not judge screenshots, verify every external reference or fact-check the prose.

Is the published thing the thing I meant to publish?

After deployment, I check the public URL as well. That catches the final gap between “the artifact was accepted” and “the page a reader gets has the expected images and links.” It is not an uptime-monitoring system; it is a last look at the actual result.

Boring does not mean free of trade-offs

The price of this setup is a build step and a dependency graph. Changes are not live until they have been built and deployed. A publishing failure is still a failure, even if there is no application process to restart.

And the source repository is part of the publishing surface. Git history is not a bin for secrets or private photographs. For Gallery of Things, the copies prepared for publication have meaningful filenames and stripped metadata; the originals stay outside the repository. Hiding a caption would not make an uploaded image private.

What I get in return is a setup I can explain: ordinary content files, my own layout, a repeatable build and a static host. If I want to change the design, I change the templates. If I want to write, I should mostly be able to stay in the article.

Lechu, Maniek, Patryk: start with that. Keep the publishing pipeline small enough that you can describe it without opening a slide deck. Spend the rest of the time making something worth writing about.

The writing can be weird.
The publishing does not need to be.

Sources and the actual wiring

The implementation details above come from this site’s current configuration. The documentation below supports the platform behaviour; it does not imply that every feature described in those guides is enabled here.

  1. GitHub: What is GitHub Pages? — static hosting and custom-domain support.
  2. Astro: Components — HTML output without a client-side component runtime by default.
  3. Astro: Content Collections and pagination reference — structured content and generated archive pages.
  4. Astro: Images — processing source images and preserving public assets as-is.
  5. Astro: CLI reference — what dev, check, build and preview actually do.
  6. Astro: Deploy to GitHub Pages — the Actions publishing source and domain setup.
  7. Cloudflare: API overview and DNS proxy status — programmatic configuration and the distinction between DNS-only records and proxied HTTP traffic.
  8. This site’s workflow, content schema and Astro configuration — the small print behind this particular implementation.

EDIT: RSS, without another moving part

Shortly after publishing this, I noticed the missing bit: there was no way to subscribe. A blog should not require remembering to visit it. So there is now an RSS feed.

I added Astro’s official @astrojs/rss helper and a static endpoint at src/pages/rss.xml.ts. At build time, it reads the same posts collection as the homepage, sorts it in the same order, and generates dist/rss.xml. Each entry contains the title, publication date, description and an absolute link to the article. It includes all published posts, not just the ten that fit on the first archive page.

This is a summary feed, not a second rendering of the whole blog inside a reader. The full articles, sketches and photographs stay on the site. The article URL also serves as the entry’s stable identifier, so rebuilding the site does not turn the same post into a new item.

A rel="alternate" link in the shared page head lets feed-aware readers discover it. The tiny rss link at the bottom left is for humans. And because the XML is just another build artifact, the existing GitHub Actions workflow checks and publishes it alongside everything else. No extra server, scheduled job or manually maintained list of posts.

A small omission fixed with one endpoint. That is exactly the sort of boring I want from the publishing side of this thing.