Why People Go Looking for This
The search usually starts after one of a few frustrating moments. You add a new page to your site, push it, then realize two weeks later it has zero analytics data — because you forgot to paste the tracking snippet in. Or you need to swap out one script across forty files and the only option is find-and-replace across your whole repo and hoping nothing breaks. Or you read about a GDPR cookie requirement and realize there's no practical way to add a banner sitewide without touching every file individually.
The underlying problem is the same in all three cases: you have site-wide code that belongs on every page, but your site is made of individual HTML files rather than a templating system that generates them. WordPress users don't have this problem — they just drop something in footer.php. If your site is flat HTML files hosted on Cloudflare Pages, GitHub Pages, or Netlify, you need a different approach.
What a YML File Actually Is
Before getting into the how, it's worth demystifying the file format, because yml (or YAML) files intimidate people who've never seen one despite being extremely simple. YAML stands for "YAML Ain't Markup Language" — a naming choice that should disqualify whoever made it, but here we are.
A .yml file is just a structured text file for configuration. It uses indentation (spaces, not tabs) to show hierarchy, and colons to separate keys from values. When you create a file at .github/workflows/something.yml in your GitHub repository, GitHub reads it and knows it's an automation instruction. That's a GitHub Action. The file tells GitHub: "when this event happens, run these steps on a temporary server."
You don't need to understand YAML deeply to use it. You need to understand it about as much as you need to understand HTML to edit a webpage — which is to say, enough to not break the indentation.
The "Just Edit the Footer File" Approach — and Why It Falls Short
The obvious first instinct for site-wide code is to put it in a shared footer file. If your site uses a build system like Jekyll, Hugo, or Eleventy, this works perfectly — those tools compile your templates into HTML and the footer gets included in every output file automatically.
But a lot of sites — especially fast, lightweight ones deployed directly from a GitHub repo — don't use a build system at all. They're just folders of HTML files. There's no "footer file" that gets included anywhere, because nothing is building anything. Each HTML file is self-contained and gets served exactly as written.
| Approach | Works for static HTML files? | Scales to new pages? | Setup time |
|---|---|---|---|
| Edit each file manually | Yes | No — manual every time | Seconds per file, forever |
| Shared footer template (Jekyll/Hugo/etc.) | Only if using a build system | Yes | Moderate — requires framework setup |
| Cloudflare Transform Rule | Yes, but limited | Yes | Easy — but requires paid plan for HTML injection |
| GitHub Action (this article) | Yes | Yes — automatically | ~3 minutes, one time |
It's also worth noting a common misconception about Cloudflare: you might expect that since Cloudflare sits between your visitors and your server, it could inject a script tag into every page as it passes through. Cloudflare can do HTML injection — but it requires a Cloudflare Worker with specific configuration, not a simple dashboard toggle. For most small site owners, the GitHub Action is easier and free.
How the GitHub Action Actually Works
When you push code to your repository's main branch, GitHub spins up a temporary Ubuntu server, checks out your code onto it, and runs whatever steps you've defined in your workflow file. In the case of a code-injection Action, those steps are straightforward:
- Find every HTML file in the repository using the Unix
findcommand - Check each file for whether the snippet is already present (to avoid duplicating it)
- Insert the snippet before the
</body>tag usingsed, a basic text-processing tool - Commit the changes back to the repository automatically
The whole operation runs in about 10–15 seconds for a small-to-medium site. The first time it runs, it patches every existing HTML file. Every subsequent run, it only touches files that are missing the snippet — so new pages get covered automatically, and existing ones are left alone.
What It Costs to Run This
For public repositories, GitHub Actions is completely free with no practical limits for tasks like this. For private repositories, GitHub gives you 2,000 free minutes per month on the free plan, and this workflow uses roughly 15–20 seconds per run. Even if you push code 20 times a day, you'd use about 10 minutes per month — well under any threshold.
The math for heavy users: 20 pushes/day × 0.33 minutes × 30 days = roughly 200 minutes/month. The free tier covers 2,000 minutes. You'd need to be pushing code 200+ times a day before you touched the limit. At that point you're probably a team and on a paid plan anyway.
If you somehow exceeded the free tier, additional minutes cost $0.008 per minute on GitHub's pay-as-you-go plan — so 1,000 extra minutes would cost about $8. This is not a bill that will surprise you.
What You Can Inject — and What You Shouldn't
The approach works for anything that lives in a <script> tag or a small block of HTML near the closing </body>. Common use cases include analytics (GoatCounter, Plausible, Fathom, Google Analytics), cookie consent banners, live chat widgets, error tracking (Sentry), A/B testing scripts, and feedback widgets.
What it's less suited for: anything that needs to go in the <head> tag (fonts, canonical URLs, meta tags), content that varies per page, or anything that requires logic — for those, you want a proper build system. This approach is deliberately blunt. It finds </body> and inserts before it. That simplicity is the feature, not a limitation.
</BODY> (uppercase) or have unusual whitespace before it, the sed replacement will miss them. A quick lowercase pass or adjusted regex handles this — but for most modern HTML files it's a non-issue.
The Cloudflare KV Warning That Prompted This Article
One thing worth addressing for Cloudflare users specifically: if you got an email from Cloudflare warning that you've hit 50% of your daily Workers KV free tier limit, and you're wondering if there's a connection — there isn't a direct one. The GitHub Action modifies files in your GitHub repository and commits them back; it has no interaction with Cloudflare KV at all.
Workers KV is Cloudflare's global key-value store, and on the free tier you get 100,000 reads and 1,000 writes per day. If you're using KV to store bug reports, user sessions, access logs, or any data that writes on every user action, those writes add up quickly. A 50% warning means you're at roughly 500 KV write operations for the day — which for a small site suggests either a bug causing excess writes, or growth that may warrant the $5/month paid Workers plan, which includes 1 million write operations per month.
Why This Saves Real Time Over the Long Run
The time savings aren't in the initial setup — three minutes is three minutes. The savings compound over every page you add after that. If you publish two blog posts a week and each one requires you to remember to add an analytics snippet, cookie notice, or other site-wide code, you're making that decision 100 times a year. Multiply that by the probability that you'll eventually forget once, or paste it in the wrong place, or update the snippet in 47 of 48 files and miss one — and the automation pays for itself in avoided annoyance within the first month.
There's also a less obvious benefit: the Action serves as documentation. The .yml file in your repo is a permanent record of exactly what code is being injected and how. Six months from now, when you can't remember why there's a GoatCounter script on every page or whether it was added consistently, the workflow file tells the whole story in 25 lines.
The One Annoying Thing Nobody Warns You About
If you use GitHub Desktop to manage your repository — which is completely normal and fine — this workflow introduces a friction point that will bite you the first few times until it becomes muscle memory.
Here's what happens: you make changes locally, open GitHub Desktop, and hit Push. GitHub rejects it. The error tells you the remote is ahead of your local branch. What actually happened is that the Action ran on your last push, injected the snippet into your HTML files, and committed those changes back to the repository on GitHub's servers. Your local copy doesn't have those commits yet — so now the remote and local are out of sync.
The fix is always the same two steps: Pull first, then Push. In GitHub Desktop that's "Fetch origin" followed by "Pull origin," and then you push your new changes on top. Once you do this a few times it becomes automatic — fetch, pull, push — but the first time it happens it looks like something is broken when nothing is.
If you use the command line instead of GitHub Desktop, the equivalent is git pull before git push. Some developers configure git pull --rebase as their default to keep history cleaner, but for a solo project it makes little practical difference.
The Complete Workflow File
For reference, here is the full GitHub Action workflow that handles this. Save it to .github/workflows/inject-analytics.yml in your repository and push it — it runs immediately on the first push and on every subsequent push to main.
Replace the SNIPPET value with whatever code you want injected. The grep -q "goatcounter" check should also be updated to match a unique string from your snippet so the duplicate-check works correctly for non-GoatCounter scripts.