In October 2026 we moved our own website from WordPress (Elementor, a theme and a dozen plugins) to a static Astro build served from Amazon S3 and CloudFront, with forms handled by one AWS Lambda function. All 693 public URLs kept their address, title, canonical and structured data; 23 old URLs became permanent redirects; and after a performance pass the home page scores 99 on mobile Lighthouse with a 2.0-second Largest Contentful Paint. The lessons that transfer to any migration are about preservation and verification, not the framework: inventory everything the old site does, script the comparison, and expect the integrations (mail, newsletter, consent) to take longer than the pages.
Key takeaways
- The migration preserved 693 URLs, 2,089 text passages on page-builder pages and the SEO head of every page, checked by scripts rather than by eye. Google's advice for site moves is to map every old URL and keep redirects "generally at least 1 year"; keeping URLs unchanged avoids most of that risk.
- The new stack is static HTML from Astro, a private S3 bucket behind CloudFront, a CloudFront Function for redirects, one Lambda function for the five form types, DynamoDB for newsletter consent and GitHub Actions for deploys. There is no server to patch.
- The old WordPress home page scored 44 for mobile performance in PageSpeed Insights (Lighthouse lab data, 10 October 2026) with a 15.3-second Largest Contentful Paint. The new site's first build scored 77 (LCP 6.7 s) because images shipped at full size; responsive images, WebP and preloading took it to 99 and 2.0 s.
- Three things broke or stalled at launch: a missing SES permission (forms with attachments), the newsletter provider's double opt-in (since resolved with the provider), and a few minutes of 403s on the form API while CloudFront picked up a new secret.
Why we rebuilt instead of tuning WordPress
The old site was a 2016-era WordPress install: Elementor, the Sydney theme, Yoast SEO, Contact Form 7 on about 60 pages, an email-subscribers plugin on about 35, reCAPTCHA v3, a security plugin and an image-compression plugin. Every page carried the builder's markup and jQuery, the home page shipped a broken <img src="/"> and a console error, 161 pages had no meta description and 28 had the wrong one (the AWS page described an AngularJS company), and the blog templates rendered a dark, overflowing page with no footer.
Speeding up WordPress was an option. We rebuilt because the content is simple (pages, posts, a portfolio, two team entries), changes a few times a month, and the team already runs the same static-plus-Lambda shape for another site on AWS. The rule we would give a client: if the site is content plus a few forms, a static build removes the server, the plugins and most of the attack surface; if it needs logged-in users, hourly publishing or a plugin ecosystem, stay on a CMS and read our CMS comparison. Either way it is website modernisation, not a redesign.
What had to be preserved
A migration is judged on what it keeps, so the first job was an inventory. We captured the live site with the host's page-speed optimiser off, discovered URLs from the Yoast sitemap (335 URLs), the WordPress REST API and a full crawl, and ended with 697 required URLs: 1 home page, 65 pages, 226 posts, 33 portfolio items, 8 portfolio categories, 2 team pages, 26 categories, 297 tag archives (all noindex on the old site and kept so) and 39 paginated archive pages. Four were unfinished mock-ups with placeholder text; we retired them with redirects, leaving 693.
| What | Old site | New site | Check |
|---|---|---|---|
| URLs | 697 required; 48 redirects observed (40 trailing-slash, 8 real) | 693 routes, 0 missing, 0 extra; /search/ added |
Route comparison script |
| SEO head | Yoast titles, canonicals, robots, Open Graph, Twitter, JSON-LD | Identical except logged, intentional fixes | Tag-by-tag diff |
| Body text | Elementor sections on 65 pages | 2,089 text nodes, all present | Sentence comparison script |
| Blog, portfolio, team | 226 posts, 33 case studies, pagination | Every word, card and pagination link kept; templates redesigned | Script diff |
| Internal links | 11 links to URLs already 404 on the old site | 0 broken; the 11 targets redirect at the edge | Link check |
| Images | 27 uploads already 404; 47 article images hotlinked | Broken tags dropped; hotlinked images self-hosted; old /wp-content/uploads/ URLs redirect to /images/ |
Asset report |
| Forms | Contact, quick contact, estimate, résumé upload, newsletter | Same five forms, native HTML, one Lambda | End-to-end test |
| Search | WordPress search on 637 pages | Pagefind static index (323 pages) | End-to-end test |
| Sitemaps, robots.txt | Yoast's five sitemap files | Same files, dates and image entries intact; permissive robots.txt; llms.txt |
331 sitemap URLs return 200 and are indexable |
Deliberately not preserved: the comment form (no post had a comment), RSS feeds (old feed URLs redirect to /blog/) and the WordPress-only plumbing (wp-json, XML-RPC, oEmbed links, generator tags).
The stack, and why each piece is there
- Astro 7 builds plain HTML at build time; Astro calls itself "the web framework for building content-driven websites like blogs, marketing, and e-commerce" with "less client-side JavaScript to slow your site down". Every page is a JSON record (SEO head plus content blocks) rendered by our own components, so editing content is editing data.
- S3 and CloudFront. A private bucket readable only by CloudFront (Origin Access Control), served over HTTP/2 and HTTP/3 with a TLS 1.2 (2021 policy) minimum, an enforced Content Security Policy and HSTS.
- A CloudFront Function on every request: apex to
wwwin one hop, trailing slashes, lower-casing of page URLs (WordPress matched case-insensitively; S3 does not), the 23 legacy redirects, old upload URLs to/images/, feed URLs to/blog/. The site's 301 redirects live here. - One Lambda function (Node.js 22) handles the five form types and sends mail through SES, behind a honeypot, a signed time token, Cloudflare Turnstile ("smart CAPTCHA alternative", no puzzle for most visitors), same-origin checks and a concurrency cap. CloudFront adds a secret header on
/api/*and the Lambda refuses anything without it. - DynamoDB stores each newsletter signup with its consent evidence (wording shown, time, page, IP); the signup is then passed to Brevo for double opt-in. With offices in India and Canada, CASL, PIPEDA and the DPDP Act shaped this.
- CloudWatch alarms e-mail the owner on Lambda errors, failed mail, failed newsletter calls, Turnstile rejection spikes or unusual traffic.
- AWS CDK defines all of it as code; GitHub Actions deploys every push to
mainover OpenID Connect, so no long-lived AWS keys exist. - Analytics load through Google Tag Manager with Consent Mode v2 and Cookiebot, the banner shown only to EEA, UK and Swiss visitors (our GTM and consent write-up).
How we verified it: the procedure we would run again
- Freeze the source. Capture every live page as HTML with optimisers off and keep it in the repository as ground truth.
- Build the URL manifest from sitemap, CMS API and crawl; classify each URL (keep, redirect, retire); record the source's own redirects and 404s.
- Compare routes. Every manifest URL has a route in the build; the build has no unexpected routes.
- Compare SEO heads tag by tag and log every intentional difference. Ours included derived descriptions, exactly one
<h1>per page (the blog index had none, seven pages had several), unique titles for paginated pages, and a branded share image replacing a product photo Yoast had attached to 213 pages. - Compare text. Every text node of every page-builder page appears in the new page; for posts, every word.
- Check links, sitemaps and robots.txt. No internal 404s, no redirect chains, every sitemap URL indexable.
- Run layout and accessibility audits at 12 widths from 320 to 1,920 px plus phone landscape, with axe-core for WCAG 2.2 AA. Result on 13 October 2026: 0 layout findings, 0 axe violations.
- Test behaviour: menus by mouse and keyboard, skip link, reduced motion, search, all five forms against the real handler.
- Measure performance with Lighthouse on 12 page types, mobile and desktop, median of three runs, before and after fixes.
Steps 3 to 6 are the ones most relaunches skip and the ones that protect rankings; our relaunch audit checklist covers them for any platform.
Performance: the numbers, honestly labelled
Before is the old WordPress home page, tested with PageSpeed Insights by our site-audit tool: Lighthouse, simulated mobile, lab data only (no field data). After is the new home page on the live domain, tested with the Lighthouse 13.5.0 command-line tool on 13 October 2026, simulated mobile, median of three runs. Both come from the same Lighthouse engine with the same default mobile throttling (150 ms round trip, about 1.6 Mbps, 4x CPU slowdown). The difference: PageSpeed Insights runs on Google's servers and the before figures are single runs, so read small gaps as noise and large ones as real.
| Home page, mobile | Before: WordPress, 10 Oct 2026 (PSI) | After: Astro, 13 Oct 2026 (Lighthouse CLI) |
|---|---|---|
| Performance | 44 | 99 |
| Accessibility | 91 | 100 |
| Best Practices | 73 | 77 |
| SEO | 85 | 100 |
| Largest Contentful Paint | 15.3 s | 2.0 s |
| First Contentful Paint | 4.5 s | 1.4 s |
| Speed Index | 16.0 s | 2.0 s |
| Total Blocking Time | 476 ms | 38 ms |
| Cumulative Layout Shift | 0.005 | 0 |
The 10 October run was not an outlier: PageSpeed Insights on the old site on 12 and 13 October 2026 gave performance 54 and 53, LCP 15.1 s and 15.3 s, and TBT 189 ms and 236 ms.
The new site did not start at 99. Its first build against the live site after the optimisation pass (Lighthouse CLI, 13 October 2026, mobile, median of three runs):
- Home page: performance 77 to 99; accessibility 94 to 100; LCP 6.7 s to 2.0 s; page weight 2,093 KB to 463 KB.
- Blog index: performance 88 to 99; LCP 3.9 s to 2.0 s.
- Desktop home page: performance 98 to 100.
What changed: hero photos and card images had shipped as full-size PNG and JPEG. We generated resized copies and WebP twins at build time (originals keep their URLs), added srcset, preloaded the first background image and the fonts, and hosted the 47 hotlinked article images ourselves. Two scores stayed put on purpose: Best Practices is 77 on pages that load the Google Ads tag (a third-party cookie), and /search/ scores 66 on SEO because it is noindex.
What went wrong
- Mail with attachments failed on launch day. The Lambda's IAM policy allowed
ses:SendEmailbut notses:SendRawEmail, which a message with a résumé attached needs. Fixed the same day; a test attachment through the deployed stack would have caught it. - Double opt-in stalled. At launch the newsletter provider's API rejected every confirmation template with "An active DOI template does not exist". Signups were stored and the owner notified in the meantime, and the ticket was resolved with the provider. Test the provider's confirmation flow from the deployed stack before cutover.
- A few minutes of 403s on the form API. The Lambda requires the origin-verify header seconds after a deploy; CloudFront takes minutes to start sending it. Infrastructure deploys first; the window is expected.
- Tooling surprises. The CDK bootstrap bucket had been deleted and had to be recreated; GitHub's OIDC subject includes numeric organisation and repository IDs the trust policy must match.
- Tracking changed four times in the week after launch: direct gtag, Tag Manager, gtag again, then Tag Manager with Consent Mode v2. Decide consent and tagging before the cutover.
What it costs
The running cost is S3 storage, CloudFront requests and transfer, Lambda invocations (form submissions only), DynamoDB on-demand reads and writes, one Secrets Manager secret, CloudWatch alarms and a month of logs. The old WordPress site ran on a small VPS at about $20 a month (our approximate figure). The new stack is not free: at current traffic it comes to a few dollars a month, which is an estimate; we will update this section after the first full month's bill. The AWS pricing pages in the sources are the reference.
A checklist for your own migration
- Inventory first: URLs, redirects, forms, feeds, search, analytics, consent, third-party scripts. Keep URLs; if you must change them, Google says to map every old URL, redirect straight to the final destination, and keep the redirects "generally at least 1 year".
- Script the comparison (routes, heads, text, links) and keep the scripts.
- Test integrations from the deployed stack; decide tagging and consent before cutover; fix images straight after the first deploy.
FAQ
Will I lose rankings if I move from WordPress to a static site?
Not from the move itself, provided every URL keeps its address, title, canonical and structured data, and every retired URL gets a permanent redirect. Google warns to "expect temporary fluctuation in site ranking during the move" when URLs change; keeping URLs unchanged, as we did, avoids the risk that guidance exists for.
How long does a migration like this take?
The build is the smaller part; inventory, verification scripts and integrations (mail, newsletter, consent, tracking) take the rest. For our own site, first capture to cutover took a few working days (DNS switched on 13 October 2026), and fixes continued through the following week. For a client site we scope a discovery first and quote the build from the inventory.
What happens to forms and newsletters without WordPress?
They become plain HTML forms posting to a small serverless function that validates, checks for spam, sends e-mail and stores records. Our five form types, including a résumé upload, run on one Lambda function; newsletter consent is stored in DynamoDB and passed to the mailing provider for double opt-in.
Do I need AWS for this?
No. The same shape (static build, CDN, a small function for forms) exists on other providers. We use AWS because the team already runs this stack and CloudFront, Lambda, SES and DynamoDB cover every piece without extra vendors. See our AWS services page.
Why not just speed up WordPress?
If your site needs daily publishing, logged-in users or specific plugins, do that: caching, image optimisation and fewer plugins go a long way. For a content site with a few forms, removing the server, the builder and the plugins removes the maintenance and most of the security surface.
Impinge Solutions moves WordPress, Wix and Webflow sites to modern stacks as part of its website modernisation service. Request a migration estimate and we will start with the inventory.
Sources
- Impinge Solutions migration repository (internal, 13 October 2026):
migration/MIGRATION-AUDIT.md,migration/EXCEPTIONS.md,migration/audit/lighthouse/summary-local-before.jsonandsummary-live-after.json(Lighthouse 13.5.0),docs/DEPLOY.md,infra/lib/site-stack.ts. - Impinge Solutions site-audit tool records (internal): PageSpeed Insights runs on the old WordPress site, 10, 12 and 13 October 2026.
- Google Search Central, "Site moves with URL changes": https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes
- Astro, "Why Astro?": https://docs.astro.build/en/concepts/why-astro/
- Pagefind, static search library: https://pagefind.app/
- Cloudflare Turnstile documentation: https://developers.cloudflare.com/turnstile/
- Amazon CloudFront pricing: https://aws.amazon.com/cloudfront/pricing/
- AWS Lambda pricing: https://aws.amazon.com/lambda/pricing/
- Amazon S3 pricing: https://aws.amazon.com/s3/pricing/


