I ran into a privacy hole recently that ships quietly with almost every static blog, and I'd never thought about it until it bit me.
If your blog is deployed from a Git repository — GitHub, in most cases — you're exposed to this.
Here's the scenario. You publish a post, commit it, push it, the deploy goes through. Then you change your mind and decide nobody should ever read it. So you delete the file locally, commit the deletion, push, redeploy. The post is gone from the site. Fixed, right?
If you want that post to actually stop existing on the internet, no — that does nothing.
The problem is Git, not the static site generator. Any blog whose content lives in Git — Astro, Hugo, Jekyll, Hexo, all of them — has the same issue: once a file has been pushed to a remote, genuinely erasing it means rewriting history.
What that looks like depends on how far the file got.
Fair warning: the commands below were written by a language model. Use them carefully.
Removing it at each stage
Deleted locally, never staged
Still just a working-directory file. Delete it. No Git commands needed.
Committed but not pushed
Use git commit --amend to rewrite the last commit without the file, or git reset HEAD~1 to undo the commit, remove the file, and commit again.
Pushed once, only a single commit involved
Run git rebase -i, drop or squash the commit containing the file, then git push origin master --force. That's enough — no need for the heavier filter-branch machinery.
Multiple commits, pushed long ago
This is where you have to rewrite history:
# Option 1 (recommended; requires git-filter-repo)
pip3 install git-filter-repo
git filter-repo --path src/content/blog/xxx.md --invert-paths
# Option 2 (classic approach, built into git)
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch "path/to/file"' \
--prune-empty --tag-name-filter cat -- --all
Afterwards, clean up what's left locally:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin master --force
The reason this is such a chore is baked into Git's design philosophy: never lose data. Every commit is an immutable snapshot. Your later "deletion" only removes the file from the newest snapshot — all the earlier snapshots are still sitting in the repository, and without a history rewrite they will stay there forever.
Habits that keep you out of this mess
Having been through the cleanup, I've settled on a few practices:
- Don't commit before publishing. Finish the draft locally and leave it alone; commit once, when you've decided it's actually going out. Drafts stay entirely outside Git.
- Catch it before the push. If something's wrong after committing but before pushing, fix it immediately with
git commit --amendor roll back withgit reset HEAD~1. - If a sensitive file only has one commit behind it, use
git rebase -ito drop that commit. Far lighter than filter-branch. - Put drafts in
.gitignore. For example, name everything undersrc/content/blog/with a.draft.mdsuffix and add*.draft.mdto.gitignore, so a draft can never be committed by accident. - Work on a local branch. Write on a local
draftbranch and merge into the main branch only when you're sure. Even if you push the draft branch by mistake, the main branch's history stays clean.
That said, the real fix is simpler than any of this: read it twice before you hit commit. Most of the time, what you want to delete later is something you shouldn't have published in the first place.