simplyjems.com is a small Canadian artisan jewelry site. Its domain was registered around 2014, and for most of its life it ran as a Joomla store with VirtueMart handling the catalog. By the time the rebuild started, the store had been offline for years — the only recoverable copy of the content was a Wayback Machine archive from 2015-2016.
In five working sessions over September 18-23, 2026, the site became a generated static site: Markdown files and templates built by a Python script, deployed by GitLab CI to ordinary shared hosting, with Cloudflare in front and a single PHP file for forms. This is the account of what that actually involved — including the parts that broke — for anyone weighing the same move. The strategic case is covered separately in our WordPress vs. static comparison; this is the engineering record.
What we started with
The old site was not just dated — it was gone. Joomla and VirtueMart had powered a real store, but the hosting had lapsed and the only surviving copy was the Internet Archive's snapshots. That meant the first step was not migration but archaeology: pulling the archived pages, extracting the text that still mattered (product descriptions, the about page, contact details), and accepting that the catalog itself would not come back.
That last point matters. The rebuild deliberately changed the site's scope from "store" to "brochure and editorial site." Every decision that follows looks easy partly because no shopping cart, checkout, or customer accounts had to be recreated.
Why static plus CI
The requirements after the scope cut were modest: about a dozen pages and articles, a contact form, a newsletter signup, and near-zero maintenance appetite. The chosen stack:
| Layer | Choice | Why |
|---|---|---|
| Content | Markdown files with YAML metadata | Editable by anyone comfortable with a text file |
| Layout | Jinja2 templates + shared nav config | Header/footer written once, not per page |
| Build | tools/build.py (Python, four pinned dependencies) |
Produces the whole site as plain files |
| Validation | tools/check.py |
Fails the build on broken links, oversized images, bad metadata |
| Deploy | GitLab CI on push to main |
lftp mirror to the web root over SFTP |
| Hosting | DreamHost shared hosting | Already paid for; static files need nothing fancy |
| Edge | Cloudflare (DNS, TLS, cache purge) | Free tier covers it |
| Forms | One contact.php endpoint |
The only PHP on the site |
The deploy mirrors dist/ to the web root with --delete, so rollback is git revert plus a push — whatever the bad commit added, the next deploy removes.
The part worth stealing: mechanical enforcement
The most durable decision was putting the site's rules in check.py instead of in anybody's memory. Every build is verified for, among other things:
- Every image under 250 KB; article images exactly 1200x800 pixels
- Article images unique — checked by both URL and file bytes
- Internal links that resolve; exactly one title, description, and H1 per page
- Sitemap, RSS feed, and
.htaccesscoverage for every page - Form markup contract — honeypot field and Turnstile widget present and consistent
- Article body word counts and meta description lengths
The enforcement is why the site stays fast: nothing depends on remembering a rule. When a duplicate hero image once slipped through because it had been re-encoded — same photo, different bytes, so the hash check missed it — the fix was not "be more careful" but extending the check to catch it.
URLs and redirects: the part that protects rankings
The rebuild changed every URL on the site — from Joomla's structure and legacy .html pages to trailing-slash directories like /about/. Two mechanisms cover the old addresses:
- Named legacy redirects. A
redirects.yamlmaps the URLs that still mattered —/bracelets,/earrings,/necklacesto/gemstones/;/jewelry-for-girlsto/about/— into real 301 rules. - Generated redirects. The build emits a 301 for every public URL's
.htmlform, soabout.htmlpermanently forwards to/about/without anyone writing a rule.
The CI pipeline then proves it works: a smoke test asserts the homepage and sitemap return 200 and that /about.html returns a 301 — and a tokenless POST to the form endpoint must return 403, which verifies the spam defenses are alive. The full playbook for this kind of URL change is our redirect checklist.
Forms without a CMS
The one thing static files cannot do is receive a POST. The solution is a single PHP file on the shared host — the same PHP that was already there — handling both contact and newsletter submissions. It layers a honeypot field (bots fill it, humans never see it) and Cloudflare Turnstile verification checked server-side, then mails the contact form and appends newsletter signups to a CSV stored above the web root. Submissions work without JavaScript via a plain redirect; with it, they post via fetch. The reasoning behind that anti-spam stack is in our contact form spam guide.
What actually went wrong
The honest bits:
- Cloudflare blocked the image fetcher. The Pexels API client got a 403 (error 1010) on every call — Cloudflare bans Python urllib's default user agent. Fixed by sending a browser User-Agent; a debugging hour on a one-line problem.
- TLS mode mattered. Cloudflare's "Flexible" SSL would have caused redirect loops against the host's own certificate; "Full" mode avoided that. The choice is recorded in the work log as deliberate, not discovered the hard way — this time.
- A
.envtypo broke secret loading. One line used:instead of=, silently dropping a credential. Small, but the kind of thing that eats an afternoon. - A duplicate image evaded the hash check (above) — the validator got smarter.
None of these are horror stories, which is roughly the point: the failures were plumbing issues found by checks and smoke tests, not weeks of CMS archaeology.
Was it worth it
For this site, yes — the result is a site whose entire surface area is static files plus one PHP endpoint, on entry-tier shared hosting, where "deploy" means pushing a commit and "the site got hacked through a plugin" is no longer a category of problem. The caveats are equally real: there is no admin panel, content edits mean editing files or Markdown, and the build tooling needs a developer occasionally. And to repeat the caveat from the top: the store was not migrated, it was retired. If your site takes payments, that is a different project.
If your site is similarly sized and you are weighing rebuild vs. repair, the refresh-or-rebuild test will put a score on it, and the tools page has the triage wizard for diagnosing what is actually wrong today.
— Editorial team. Facts current as of 2026; we revise guides when the ground shifts.
Frequently Asked Questions
How long did the rebuild actually take?
Five working sessions across September 18-23, 2026 — from DNS takeover to a CI-deployed site with seven published articles. A first article-writing pass ran alongside the engineering work.
What happened to the old store pages?
The VirtueMart catalog was not rebuilt — the business no longer sells online. Named legacy URLs like /bracelets and /earrings redirect to the closest current page, and every old .html address gets a generated 301 to its trailing-slash equivalent.
Would this approach work for a bigger site?
The architecture scales fine — a static build handles hundreds of pages comfortably. The constraint is editorial: content that changes daily or needs an admin panel for several editors belongs in a CMS, which is the trade-off our WordPress comparison walks through.