Why static export + cPanel

Not everyone has the budget or inclination to run a Node.js process on a server. cPanel shared hosting is cheap, reliable, and already paid for by a lot of people. With output: 'export', Next.js produces a folder of static HTML, CSS, and JS that cPanel can serve with no extra configuration.

The trade-offs are real: no server-side rendering at request time, no API routes that require a running server, no streaming. For a portfolio and blog, none of that matters.

The pipeline

- 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 }}

That's it. The action does a diff on every deploy so it only uploads changed files. Important when you're on shared hosting with FTP rate limits.

The gotchas

Trailing slashes

Set trailingSlash: true in next.config.ts or cPanel will return 404s for routes like /writing instead of serving writing/index.html.

Image optimisation

Set images: { unoptimized: true }. The Next.js image optimisation pipeline requires a running server, so it has no place in a static export.

dangerous-clean-slate

Start with it set to true on first deploy to clear any leftover files, then set it back to false. If you leave it true, every deploy wipes the whole directory before uploading. Fine in theory, but it means a brief window where your site is empty.

More detail coming in a follow-up post.

All writing