Log In
← The Export

The Three-Minute GitHub Action That Injects Code Into Every Page You'll Ever Deploy

You set it up once. After that, every HTML file you push — today, next month, whenever — automatically gets your analytics snippet, cookie notice, or any other code block injected before it hits production. No shared templates. No manual copy-paste. No "oh, I forgot to add it to the new page." Here's exactly how it works and whether it's right for your project.

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:

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.

The idempotency trick: The Action checks for the snippet before injecting it. This means you can run it a hundred times and never end up with duplicate script tags. In automation, an operation that produces the same result no matter how many times you run it is called idempotent — and it's what separates a reliable workflow from a messy one.

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.

Watch out: If any of your HTML files use a non-standard closing tag like </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.

The silver lining: This pull-before-push habit is actually good version control hygiene regardless of automation. It keeps your local copy in sync with the remote before you layer on new changes, which prevents the kind of merge conflicts that are genuinely painful to untangle. The Action is just enforcing a workflow you should probably be doing anyway.

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.

name: Inject Analytics on: push: branches: [main] workflow_dispatch: jobs: inject-analytics: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Inject snippet run: | SNIPPET='<script data-goatcounter="https://yoursite.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>' find . -name "*.html" \ -not -path "./.git/*" \ -not -path "./node_modules/*" \ | while read file; do if grep -q "goatcounter" "$file"; then echo "Already has snippet: $file" else sed -i "s|</body>|$SNIPPET</body>|g" "$file" echo "Injected: $file" fi done - name: Commit changes run: | git config user.name "github-actions" git config user.email "[email protected]" git add -A git diff --staged --quiet || git commit -m "chore: inject analytics" git push

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.

See our free AI tools →