All notes

What goes to the server
and what stays home

A repository usually holds more than a website: sketches, prompts, reference photos and tools. The server should get the website and nothing else, and a deploy should be boring enough to run on a Monday morning.

Straight to the code

The repository is not the website

Next to the pages, a repository easily collects design sources, image prompts, reference photos, notes and tools. Upload the whole folder, and all of that becomes public along with the site.

  • Goes to the server: the finished pages, the styles, scripts, fonts and images they actually use, and the server files such as robots.txt, the sitemap and redirect rules.
  • Stays home: design sources and prompts, personal photos, notes, tests and build tools, backups, and anything with a password or a key in it.

That is why our site is built into a separate folder. The builder renders every page in each language at a clean address, such as /pl/notes/, then copies only the files that those pages, the stylesheet and the scripts reference. Whatever is not linked does not travel.

Names that beat the cache

One surprise along the way: a CDN in front of a site ignored the familiar ?v= trick, and the old stylesheet kept coming back after an update. So the builder renames the files instead. Each new name carries the first eight characters of a SHA-256 hash of the file’s content.

JavaScript · tools/build-site.cjs

for (const file of HASHED) {
  const text = readText(file);
  const hash = crypto.createHash('sha256').update(text).digest('hex').slice(0, 8);
  renamed[file] = file.replace(/(\.[a-z]+)$/, `.${hash}$1`);
  write(renamed[file], text);
}

readText() also turns Windows line endings into plain ones, so the same content gets the same name on any computer. The pages are then rewritten to point at style.1a2b3c4d.css instead of style.css; the source files never change.

Now the caching rules can be strict. Pages always revalidate, so a deploy is visible on the next visit. A hashed stylesheet or script is cached for a year as immutable: a changed file gets a new name, so the old one never goes stale. Images are not renamed automatically, so a replaced image gets a new name by hand, such as -v2.

A deploy that can say no

The deploy script runs the builder and refuses to continue if the result looks incomplete. That matters when the upload mirrors a folder with deletion, as rsync --delete does: mirrored from an empty folder, it would empty the live site.

Bash · tools/deploy.sh

test -f dist/site/index.html || { echo "deploy: no index.html in dist/site"; exit 1; }
test -f dist/site/.htaccess || { echo "deploy: no .htaccess in dist/site"; exit 1; }
pages=$(find dist/site -name '*.html' | wc -l)

The next line compares that number with the expected count of pages: every page in each language plus the 404. Only then does anything leave the computer.

  1. Back up first
    Before anything changes, the current version of the site is packed into a backup. A few recent copies are enough.
  2. Unpack next to the live site
    The new build lands in a temporary folder first, so a broken upload never replaces a working site halfway.
  3. Touch only your own folder
    The sync updates the site’s own folder and nothing next to it.

Keys stay out of it. A deploy key or password belongs neither in the repository nor in the build: it lives only where the deploy is started.

Ask the live site, not the build

After the upload, the script asks the real domain a few questions and expects exact answers.

  • The homepage and a page in each language answer 200: every version is in place.
  • The contact form handler answers 405 to a plain visit: it accepts only POST.
  • An old address answers 301: links people saved still lead somewhere.
  • A made-up address answers 404: a missing page is reported honestly, with the site’s own 404 page.

Another surprise: a CDN may answer a bare scripted request with 403 while serving everyone else. So each question gets five attempts before it counts as a failure.

Bash · tools/deploy.sh

check() {
  for attempt in 1 2 3 4 5; do
    code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$1")
    if [ "$code" = "$2" ]; then echo "$1 -> $code"; return 0; fi
    sleep 6
  done
  echo "deploy: $1 answered $code, expected $2"; return 1
}

Boring on purpose

A good deploy is uneventful: the same build every time, a fresh backup and a handful of exact answers from the live site. If something is off, the script stops before the visitors notice. Kotka approves of anything that lets her nap through release day.

All notesAlso: a cover shows the real project