# The stack I use to run this blog, 2026 edition

> How I run flaviocopes.com in 2026: Astro, Tailwind, Cloudflare Pages, and the small services around a folder of Markdown files.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-09-22 | Topics: [Blogging](https://flaviocopes.com/tags/blogging/) | Canonical: https://flaviocopes.com/blog-stack-2026/

flaviocopes.com in 2026 is a folder of Markdown files, an Astro build, and Cloudflare Pages serving the output. Around that sit a few serverless functions, some storage, a Worker that rebuilds the site through the day, search, analytics, the newsletter, and email.

I wrote [the 2018 version](https://flaviocopes.com/stack/) of this post when the site ran on Hugo and Netlify. It got patched over the years, but it's still a 2018 post. Here is what runs today, and why each piece is there. The longer write-ups are linked where I have them.

## 1. Astro

The site is a static Astro build. The build writes plain HTML. No adapter, and no server rendering. Posts are a content collection, a glob over the posts folder, with a schema for the frontmatter.

Drafts and future dates stay out of the build. I can also keep a URL live and leave it out of the sitemap and the listings. Missing a title or a description fails the build, which is what I want on a site this size.

The Markdown pipeline is the usual one, plus two plugins I wrote. One puts real width and height on images, and lazy loading, so pages don't jump. The other drops a promo box into longer posts, at build time. A few pages are MDX, because they need components. RSS and the sitemap come from the official integrations.

[Why I use Astro](https://flaviocopes.com/why-i-use-astro/) is from 2023 and still holds, and there's a free [Astro course](https://flaviocopes.com/courses/astro/).

Build time is the thing to watch. The site renders thousands of pages. How I brought that down is in [how I made this site build faster](https://flaviocopes.com/optimize-astro-build-deploy-time/).

## 2. Tailwind

Styling is Tailwind, with the typography plugin for post bodies.

The look is a newspaper. Uppercase headings, thin borders, no rounded corners. Colors live in CSS custom properties, and dark mode is a class on the root that swaps them. The font is Geist Mono, self-hosted, so there's no request to a font host.

One spacing utility bit me on the tools section. A reset in a scoped stylesheet beat it, and some fieldsets collapsed. It took a while to see why. The free [Tailwind course](https://flaviocopes.com/courses/tailwind/) covers the current version.

## 3. Cloudflare Pages

Hosting moved from Netlify to Cloudflare Pages this year. The site was already almost entirely static, DNS was already on Cloudflare, and the Netlify-specific pieces were a couple of functions plus a small blob store. Pages also doesn't bill for bandwidth.

The project is connected to the GitHub repo and builds on every push to `master`. That build checks the downloadable courses are still in sync, builds the site, then indexes it for search.

Settings live in a Wrangler config file, not in the dashboard. The project name in that file has to match, or Git builds fail. Once the output folder is declared there, the dashboard stops being the source of truth, including which Node version the build uses. The default image ships a Node that's too old for current Astro, so I pin a current one.

Books and course files are too big for the build output, which is why they live in object storage. More in [the Cloudflare products I actually use](https://flaviocopes.com/cloudflare-products-i-use/) and in [how the Pages build cache works](https://flaviocopes.com/cloudflare-pages-build-cache/).

## 4. A few serverless functions

Anything that can't be a static file is a small function. There are a handful.

The purchase webhook assigns course access, and past students can ask for their links again. Signup, the sponsor form, and the one tool that calls a model go through the same kind of function.

Shared helpers live outside the functions folder, so the host doesn't publish them as routes. The purchase webhook checks a signature, which is why Node compatibility is turned on for the project.

I tried a middleware that ran on every request. Images and CSS went through it too, so each page view became a metered call. I took it out. Now only those few routes run code.

## 5. Storage, and the CLI around it

Two key-value namespaces. One maps a buyer's email to the course links they own. The other holds short-lived counters, so a form or the AI tool can slow down abuse. That store is eventually consistent, which is fine for course links and abuse counters, and a bad fit for a number that has to be exact.

Object storage holds the books, and a PDF and EPUB of every free course, on its own subdomain. The repo keeps a list of the files that should be there. The production build fails if a course is out of date. Uploading those files is a separate step from building the site. There's no egress fee, which matters when people download a lot of PDFs.

The CLI for all of this is Wrangler. Secrets go in through that, never into the repo. I have short guides on [KV](https://flaviocopes.com/cloudflare-kv/), [R2](https://flaviocopes.com/cloudflare-r2/) and [Wrangler](https://flaviocopes.com/cloudflare-wrangler/), and the free [Cloudflare course](https://flaviocopes.com/courses/cloudflare/) covers the platform.

## 6. Scheduled posts, published by a Worker

A date in the future schedules a post. A static site does not rebuild itself when that time arrives.

I publish three posts a day. A scheduled Worker runs shortly after each one and asks Pages to rebuild, with a POST to a deploy hook. The Worker checks the time in Rome, so the clock change twice a year doesn't fire the job an hour off. A GitHub Action is the backup. GitHub's schedule is best effort, and I've seen it run hours late.

The setup is in [how I auto-publish scheduled posts](https://flaviocopes.com/scheduled-cloudflare-pages-rebuilds/). The annoying failure is a build stuck as active, with everything else queued behind it. I once had to [cancel one through the API](https://flaviocopes.com/fix-stuck-cloudflare-pages-build-queue/).

## 7. Pagefind for search

Search runs in the browser. After the site build, Pagefind reads the HTML and writes an index in small chunks. A visitor downloads only the chunks their query needs.

The index covers posts, free courses, tools and the general pages. Layouts mark the real content, so the header and footer don't match every search. The box lives on `/search` and on the first page of the blog. More in [how I added search to my static site](https://flaviocopes.com/static-site-search-pagefind/).

## 8. Plausible on a droplet

Analytics is Plausible's community edition, self-hosted on a DigitalOcean droplet, with Docker Compose.

I set it up in 2023. A small machine was enough, so I never moved to the hosted plan. The setup is in [how to self-host Plausible](https://flaviocopes.com/how-to-self-host-plausible-analytics/). Years later I had Codex upgrade that box: [Updating self-hosted Plausible Analytics using AI](https://flaviocopes.com/updating-plausible-with-ai/). If you want the same host, [sign up for DigitalOcean through my link](https://flaviocopes.com/go/digitalocean). That's an affiliate link, and I get credit if you become a customer.

Plausible only sees browsers that run its script, so it says nothing about AI crawlers. Cloudflare's dashboard has a report for that.

## 9. The newsletter, and transactional email

The newsletter is Sendy, a self-hosted PHP app, on a small Ubuntu server, also on DigitalOcean. It sends through Amazon SES. That pair was already in the 2018 post.

The signup path changed. Forms used to post straight to Sendy. A bot used that and subscribed a pile of addresses. Now every form hits my own function first. It checks a Turnstile token, limits how often one IP can try, and only then talks to Sendy, with a key the browser never sees. That story is [the newsletter spam attack](https://flaviocopes.com/newsletter-spam-attack/).

The same server produced two debugging posts. Codex over SSH found [a process the OOM killer kept shooting](https://flaviocopes.com/server-process-killed/), and [a send that had slowed to a crawl](https://flaviocopes.com/sendy-slow-emails-ai/).

Transactional email is a separate service, Resend, called from the functions. Course links, form mail, and the notes I send myself.

## 10. Paid courses

Paid courses go through Paddle, a merchant of record. It handles tax and invoices, and it sends the webhook that assigns access.

## 11. Markdown for agents, and social images

Every published post also exists as Markdown, at the same URL with `.md` on the end.

```bash
curl https://flaviocopes.com/npm.md
```

The build writes one of those files per post, and each HTML page points at its copy. There's a short index and a full one, both static, so this costs nothing at request time. Doing it at the edge would need a paid Cloudflare plan. I do it at build time. The reasoning is in [serving my site as Markdown to AI agents](https://flaviocopes.com/serving-markdown-to-ai-agents/). The robots file allows the AI crawlers, which I explain in [robots.txt in the age of AI crawlers](https://flaviocopes.com/robots-txt-ai-crawlers/).

For a while every post got its own generated social image. Deploys got heavy, so articles now share one compact image, and a newer post can bring its own. Courses, tools and topic pages still get a generated card. This post has a custom one. The old setup is in [how I generated Open Graph images](https://flaviocopes.com/generate-og-images-astro/).

## 12. Sponsors

A column on the site lists sponsors. On a phone it's a thin strip. On a wide screen it sticks to the right while you scroll. The current one is <a href="https://www.creem.io/?utm_source=flaviocopes.com&utm_medium=sponsor" rel="sponsored noopener">Creem</a>.

## 13. Browser tools

There's a tools section. Each tool runs in the browser, with Alpine.js, and each one lives in its own folder.

One of them calls a model. That's the app idea generator. It checks a Turnstile token, stays under a daily cap, and then calls a small model. The launch post is [New: 90 free tools for developers](https://flaviocopes.com/introducing-tools/), from when the section was smaller, and the free [Alpine.js course](https://flaviocopes.com/courses/alpinejs/) covers the library.

## 14. Free courses

Free courses are a second content collection. Lessons are Markdown, quizzes run in the page, and progress stays in the browser. No account.

Each course also ships as a PDF and an EPUB. I generate those separately, upload them next to the books, and the site build only checks that they're current. The build machines never need a TeX install. The launch post is [I just published 55 free courses for developers](https://flaviocopes.com/free-courses-for-developers/).

## 15. Coding agents

Most of the code in this repo is written with coding agents. Cursor day to day. Claude Code or Codex when they fit better. The Codex CLI over SSH when the work is on a server.

The repo has an `AGENTS.md` with the facts an agent would otherwise learn by breaking a build. I wrote about [what goes into that file](https://flaviocopes.com/agents-md/).

Every commit also appends a line to a plain text work log, grouped by date and project. A Git hook does it, and the agents install that hook. The setup is in [how I log every Git commit to a plain text file](https://flaviocopes.com/log-git-commits-plain-text/).

Local dev goes through [Local Hoster](https://flaviocopes.com/software/local-hoster/), a tool of mine, so the project gets a stable local HTTPS URL.

## What stayed out

Posts are Markdown in a private repo, with no CMS in front of them. I edit in an editor and publish with a push. Courses, tools and the data files work the same way.

Alpine.js shows up on the tool pages. A post is the HTML, the CSS, a dark mode toggle, and a diagram script only when the post has a diagram.

I don't run comments, and I don't run a server for the site itself. Two machines stay up, one for analytics and one for the newsletter, because that software can't be a static file. Everything else is a file in the build, or a Cloudflare service.

The Markdown files and the newsletter are still the 2018 pieces. Around them, the host, the build and the small functions are config I can open.
