3 min readby Muhammad Mohtashim Husnain

Deploying a static site to Hostinger from a script

Build, zip, upload, deploy, clear cache, verify: six steps to publish a static site on shared hosting, and what each one can get wrong.

Shared hosting is usually deployed by dragging files into a browser file manager. That works once. By the tenth deploy of the week it is slow, and it is easy to leave an old file behind. This is the routine I use instead for this site and for client sites.

The six steps

1. Build. The site is a Vite project. npm run build produces a folder of plain files, then a second script opens every route in a headless browser and saves the rendered HTML, so each page exists as a real file that search engines can read without running JavaScript.

2. Zip the build folder. The contents, not the folder itself. The archive must unpack straight into the web root.

3. Ask the hosting API for an upload address. It returns a URL and two short-lived keys. These are not the account's API token, and they expire after a few hours.

4. Upload the archive. The upload uses the TUS resumable protocol, which is two requests: one that announces the file and its size, one that sends the bytes.

SIZE=$(wc -c < site.zip)

curl -X POST "$URL/site.zip?override=true" \
  -H "X-Auth: $KEY" -H "X-Auth-Rest: $REST_KEY" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: $SIZE" -H "Upload-Offset: 0"

curl -X PATCH "$URL/site.zip?override=true" \
  -H "X-Auth: $KEY" -H "X-Auth-Rest: $REST_KEY" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Content-Type: application/offset+octet-stream" \
  -H "Upload-Offset: 0" --data-binary "@site.zip"

The first answers 201. The second answers 204 with an Upload-Offset header. If that number equals the file size, the whole archive arrived.

5. Deploy the archive and clear the cache. One API call unpacks the archive over the web root. A second clears the server and CDN cache.

6. Verify. Not with the status code.

What each step can get wrong

The deploy wipes the web root. Everything not in the archive is deleted. If the site has files that are not part of the build, such as a verification file or a folder uploaded by hand, they must live in the project's public folder or they are gone after the next deploy.

A 200 proves nothing. A fresh or failed deploy can still serve a page. After every deploy I fetch the home page and search for a string that exists only in the new build, usually the hashed name of the main script. If that is not there, the old version is still being served.

A cache can hold the old version or an error. If the new string does not appear, clear the cache again and wait a minute before deciding the deploy failed.

File names must change when content changes. The build adds a content hash to every script, stylesheet and image name, so a browser never uses an old copy of a changed file. I also keep one revision constant in the build config. Bumping it renames every file at once, which is the quickest way out when a cache somewhere is holding bad copies.

Do not check the result with a fast crawler. A script that requests every file in a second looks like an attack to a rate limiter. My verification requests a handful of pages, a few seconds apart, and then loads a few in a real browser.

Why bother

A deploy that takes one command gets done after every small fix. A deploy that takes ten minutes of clicking gets postponed, and then three changes go out together and nobody knows which one broke the page. The script also leaves a record: what was uploaded, how many bytes, what the server answered.

The whole routine is about forty lines. It has paid for itself many times this month.

Related: Two CDNs in a row: three days of 429 errors, which is what happens when the layers in front of the server are not what you think they are. The service page is company website development.