Chapter 8 of 8. Prerequisite: Chapter 7 — you have the FTP-Deploy-Action workflow building and shipping
out/with your dotfiles intact. This last chapter is the one flag on that workflow you set exactly twice: once at the start, once right after. Get the order wrong and you either ship onto a pile of stale files or wipe production a month later.
The whole course has been building to a working deploy. The final decision is a sequence, not a single config value. dangerous-clean-slate is a boolean, and the safe way to use it is to flip it on for one deploy and off for every deploy after. Nobody writes this sequence down. So people either never turn it on and inherit whatever junk was on the host, or they leave it on and delete their own site the first time an unrelated file needs to survive. Here is the order, and why each step is the way it is.
What dangerous-clean-slate actually does
The FTP-Deploy-Action action.yml defines the input in one line: it "Deletes ALL contents of server-dir, even items in excluded with exclude argument." Read that twice. When the flag is true, the action does not sync your changes onto the host. It empties server-dir first, then uploads a fresh copy of local-dir. The README settings table confirms the default is false.
Two consequences fall straight out of that sentence.
First, dangerous-clean-slate: true overrides your exclude list. In the normal false mode, exclude protects files from being touched. In clean-slate mode, exclusions save nothing in server-dir from deletion. The action wipes first and asks nothing. So a file that lives on the host but is not in your out/ folder is gone after a clean-slate deploy, regardless of what you excluded.
Second, this is exactly why the default deploy mode is incremental. With the flag false, the action compares a state file against local-dir and syncs only the difference. It leaves alone every file it did not put there. That is the mode you want for every routine deploy, because your static site is not the only thing that might live under that FTP root.
Why the first deploy needs a clean slate at all
If the host were empty, you would never need this flag. In practice the first deploy almost never lands on empty ground. A cPanel public_html/ ships with a default index.html or a "coming soon" placeholder. If you migrated a domain, the old site's files are still sitting there. If a previous framework or an earlier build attempt touched the directory, its leftovers are there too.
Incremental sync will not remove any of that. The false-mode action only reasons about files it is uploading. It has no mandate to delete a stray old-site.html or a leftover wp-content/ it never created. So your shiny new export goes up alongside the old files. The apex URL might still resolve to the host's index.html instead of your export's, or an old deep link keeps serving a page you thought you deleted. The site looks half-migrated because it is.
That is the one legitimate use for dangerous-clean-slate: true: the first deploy, to guarantee the host holds only your out/ folder and nothing inherited. You accept the danger of a full wipe in exchange for a clean starting state, and you accept it only once, while there is no real production traffic to lose yet.
The sequence, step by step
Here is the exact flow this site uses. The workflow is the one from Chapter 7; the only edits are to the one line.
1. Confirm server-dir is correct and tightly scoped. This is the single most important check, because clean-slate deletes everything under it. On this site server-dir comes from a secret, so no path is committed:
- name: Deploy via FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./out/
server-dir: ${{ secrets.FTP_SERVER_DIR }}
dangerous-clean-slate: false
Set FTP_SERVER_DIR to the exact web root you mean to fill. For a static site that is typically public_html/. If that secret is wrong, no amount of care on the flag saves you.
2. Dry-run first. Before you delete anything, prove the action is pointed where you think. The action.yml documents a dry-run input that "Prints which modifications will be made with current config options, but doesnt actually make any changes." Temporarily set both, push, and read the log:
dangerous-clean-slate: true
dry-run: true
The log lists every file it would delete and upload. Confirm the delete list is only stale host files and the upload list is your export. If the delete list contains anything you did not expect, stop and re-check server-dir before going further.
3. Do the real clean-slate deploy. Remove dry-run, keep the flag on, push to main:
dangerous-clean-slate: true
The deploy job runs only on main pushes (and workflow_dispatch), never on PRs, so this cannot fire from a pull request by accident. The host is now wiped and holds exactly your out/ folder.
4. Flip it back to false immediately. This is the step people forget. Do it in a separate commit, right after the first deploy succeeds:
dangerous-clean-slate: false
From here every deploy is incremental. This site keeps the flag false with the reasoning written directly into the workflow as a comment: "Set dangerous-clean-slate: true only on first deploy to wipe stale files, then switch back to false so incremental deploys don't nuke everything."
Two failure modes to name
Everything above exists to avoid two specific disasters. Naming them makes the sequence stick.
The first is the dirty initial deploy: leaving the flag false on the first deploy. Your export goes up over the top of whatever was there. The symptom is that the homepage or some routes serve the old host content, deep links resolve to pages you never built, and nothing you do to out/ seems to fix it. The offending file is not in out/ at all. The fix is a one-time clean-slate deploy, which is the whole point of doing it deliberately at the start.
The second is wiping production: leaving the flag true after the first deploy. Every subsequent deploy empties server-dir before uploading. A purely static site survives this, because the wipe is immediately followed by a full re-upload of the same out/. The damage appears the moment anything you did not build lands under that root. Think of a file a colleague uploaded via cPanel's file manager, a .well-known/ directory an SSL renewal dropped in, or a backup someone parked there. The next deploy silently deletes it, and exclude will not protect it because clean-slate ignores exclusions. This is the failure the "don't leave it on" rule prevents.
When you might reach for clean-slate again
The honest answer is "almost never." Once the site is live and incremental, you keep the flag false permanently. There are two narrow cases where a second deliberate clean-slate is defensible, and both are one-offs treated exactly like the first deploy: dry-run, scope-check, flip on, deploy, flip back.
The first is a structural rename that leaves orphans. If you delete a whole route or move a large section, the old dir/index.html files still sit on the host, because incremental sync uploads and updates but does not aggressively prune everything gone from out/. A single scoped clean-slate clears the orphans in one pass. The alternative, and often the safer one, is to delete the specific stale directories over FTP by hand and stay incremental.
The second is a corrupted or half-synced host after a deploy died mid-transfer. If the state is genuinely unknown, a clean-slate rebuilds it from a known-good out/. Because the site is fully static and reproducible from the build, wiping and re-uploading loses nothing that is not already in your repo.
Both cases share one precondition: you must be certain server-dir contains only files your build owns. The moment anything else lives there, clean-slate is the wrong tool and a targeted manual delete is right.
Where this leaves you
That is the full chain. Over eight chapters you decided static export fits, wrote the next.config.ts that makes it real, built an MDX pipeline that survives the export, worked around next/image, added an .htaccess for the host-level redirects and content-type fixups, handled the 404.html export bug, built the FTP deploy that keeps your dotfiles, and now sequenced the one flag that decides whether a deploy refreshes your site or empties it.
One thing worth being precise about, since it is easy to get backwards: the trailing-slash routing that lets /writing/ resolve to a page is not something the .htaccess does. Static export already emits each route as a dir/index.html file, and the web server's default directory-index behaviour serves index.html when a directory is requested. That is what makes trailingSlash: true URLs resolve. The .htaccess is for the things the server would not do on its own: the www-to-apex redirect, the RSC .txt content-type handling, the ErrorDocument for 404s, and a handful of RedirectMatch content redirects.
The lived version of all of this is the Static-export Next.js to cPanel write-up, where every gotcha in this course was hit and fixed on the site you are reading it on. The single rule to carry out of this last chapter: dangerous-clean-slate is true exactly once, at the very start, and false forever after.
Rehearse the clean-slate sequence with dry-run evidence
Execute the first-deploy sequence on your own site, but treat it as a documented rehearsal: verify the scope of server-dir before touching the flag, capture the dry-run delete list as evidence, land the wipe and the flip back as separate commits, and leave behind a workflow comment that stops the next person flipping the flag casually.
Expected behaviour
- FTP_SERVER_DIR verified against the actual web root before any flag change, since clean-slate deletes everything under it
- A dry-run log captured with dangerous-clean-slate: true and dry-run: true set together, with the delete list reviewed line by line
- The real clean-slate deploy and the flip back to false land as two separate commits, in that order
- A comment in the workflow records that the flag is true exactly once, at the start, and false forever after
- A routine follow-up push deploys incrementally without touching any file the build does not own
PROVE IT Show the dry-run log with its delete list, the two commit hashes for the flip on and the flip off, and a follow-up deploy log demonstrating an incremental sync.
With dangerous-clean-slate: true, what happens to the paths in your exclude list?
Which action input prints the modifications a deploy would make without changing anything on the host?
Name both failure modes the clean-slate sequence exists to prevent, and how each one shows up.
Show answer
The dirty initial deploy comes from leaving the flag false on the first deploy: the export lands alongside inherited host files, so routes serve old content and nothing in out/ explains it. Wiping production comes from leaving the flag true afterwards: every later deploy empties server-dir first, silently deleting anything the build does not own, and exclude offers no protection in that mode. The sequence, on once then off immediately, is what prevents both.
↺ re-read: “Two failure modes to name”