Migrating WordPress to Azure Static Web Apps

Migrating WordPress to Azure Static Web Apps: A Real Deployment Log

Why static?

This blog has run on WordPress for over a decade. Great for writing, but it's also a PHP app, a MySQL database, and a permanent stream of plugin updates and security patches for a site that is, in the end, just text and screenshots. Azure Static Web Apps serves plain HTML/CSS/JS straight from a CDN, deploys automatically from GitHub, and is free for a site this size. So: export WordPress to static files once, and never patch a database again.

Here's the actual log of what happened when we did it — including the two things that broke.


1. Export WordPress as static HTML

The Simply Static plugin crawls the live site and writes out every page as a folder with an index.html, plus wp-content/uploads, wp-content/themes and wp-includes alongside it — a byte-for-byte snapshot of what visitors already see, with none of the PHP. Download the zip, unzip it into a website/ folder inside the GitHub repo, done.


2. Connect the repo to Azure Static Web Apps

Creating the Static Web App resource in Azure and pointing it at the GitHub repo drops a ready-made GitHub Actions workflow into .github/workflows/. The only thing worth checking is app_location — it defaults to the repo root, but our export lives in a website/ subfolder:

          app_location: "/website" # App source code path
          api_location: ""
          output_location: ""

No build step, no framework — it's plain HTML, so output_location stays empty. Push to main, and the workflow builds and deploys automatically.


3. The first deploy failure: 250MB is not a lot of screenshots

The first push failed at the deploy step:

"The size of the app content was too large. The limit for this Static Web App is 262144000 bytes."

262,144,000 bytes is 250MB — the hard content limit on the Free tier. Our export was 266MB. A decade of tech-blog screenshots had quietly added up.

$ du -sh website/wp-content/uploads/*
221M  wp-content/uploads
 39M  wp-content/uploads (wp-includes)

2,823 PNGs and 735 JPEGs, mostly full-resolution UI screenshots and a handful of phone photos that had never been compressed for the web.


4. Compressing 3,500+ images without visible quality loss

Two upgrade paths existed — pay for the Standard tier, or shrink the export. Screenshots compress extremely well because they're mostly flat colour and text, so we tried ImageMagick before reaching for the credit card.

Lossless PNG recompression barely moved the needle:

magick mogrify -strip -define png:compression-level=9 \
  -define png:compression-filter=5 -define png:compression-strategy=1 *.png
# 82M -> 80M (~2% smaller)

Quantizing to a 256-colour palette, on the other hand:

magick mogrify -strip -colors 256 -define png:compression-level=9 *.png
# 82M -> 29M (~65% smaller)

For UI screenshots this is visually lossless — 256 colours is more than enough for a browser chrome and some text. JPEGs got the equivalent treatment:

magick mogrify -strip -quality 82 -sampling-factor 4:2:0 *.jpg
# one folder of phone photos: 21M -> 11M (~48% smaller)

Watch out for: batch-processing thousands of files with xargs silently skipped a couple of images whose filenames contained a Unicode middle-dot character (DALL·E-2022-...). The fix was switching from one mogrify call per batch to one call per file (xargs -I{}), which handles unusual filenames correctly at the cost of a bit of speed.

The one place we deliberately checked twice: a couple of DALL·E-generated images have smooth gradients rather than flat colour, and palette reduction can visibly band those. A side-by-side comparison confirmed no visible artefacts even there — screenshots and AI art both held up fine at 256 colours.

End result: uploads/ went from 214MB to 87MB, and the whole export from 266MB to 139MB — comfortably under the 250MB limit, with room for years of future posts.


5. Custom domain: the apex-domain gotcha

DNS for gerjon.com runs on Cloudflare. Adding www.gerjon.com as a custom domain was straightforward — a CNAME to the Static Web App's default hostname, validated automatically. Adding the bare gerjon.com apex domain was not:

"CNAME Record is invalid. Please ensure the CNAME record has been created."

Even though Cloudflare's dashboard displays a CNAME on the root record, the DNS protocol doesn't actually allow that — Cloudflare "flattens" it into a plain A record before it ever reaches a resolver. Azure's default validation checks for a literal CNAME, so on an apex domain it never finds one.

The fix is to validate via TXT token instead of CNAME delegation:

az staticwebapp hostname set \
  --name gerjon-com --resource-group gerjon-website \
  --hostname gerjon.com \
  --validation-method dns-txt-token

Azure generates a validation token, which goes into a _dnsauth TXT record at Cloudflare; once that resolves, the apex domain activates with a managed certificate. Subdomains use cname-delegation (the default); apex/root domains need dns-txt-token — that's the whole rule.


TL;DR

  • Export WordPress to static HTML with Simply Static; point the Static Web App's app_location at wherever that export lives.
  • Free tier caps content at 250MB (262,144,000 bytes). Screenshot-heavy sites hit this fast.
  • 256-colour PNG quantization and quality-82 JPEGs cut a decade of uploads by roughly 60% with no visible quality loss — try that before upgrading your plan.
  • Batch image tools can silently skip files with unusual Unicode filenames; process one file per invocation if anything looks off.
  • Subdomains validate custom domains via CNAME; apex/root domains need TXT-token validation, because CNAME flattening (Cloudflare and similar) hides the literal CNAME record Azure is looking for.

Leave a Reply