← Blog

Migration

Joomla 3 to Astro: the migration math that decided it

A Dutch dental group asked us to lift their Joomla 3 site onto Joomla 5 before the year was out. We opened a spreadsheet, ran the numbers, and rebuilt it in Astro instead.

Jacob Molkenboer· Founder · A Brand New Company· 4 Jun 2026· 9 min
Closed leather logbook, brass key on cream card with green ribbon, red wax fragment on ivory desk by window.

Late January 2026. A four-location dental group in Utrecht emailed their account manager. Their Joomla 3.10 site had been throwing yellow warnings in the admin for over a year. The booking iframe was hanging on a deprecated PHP function. Their shared host had given them ninety days to be off PHP 7.4. The ask: migrate to Joomla 5 before April.

We said yes to the call, not yet to the migration. Two hours into the audit we had a different proposal on the table: skip Joomla 5 entirely and rebuild the site as a static Astro project. The clinic group approved it on the second meeting. This is the math we showed them.

What Joomla 5 actually requires

There is a comfortable assumption that going from Joomla 3 to Joomla 5 is a long button press. It is not.

Joomla 3 reached end of life on 17 August 2023. Security fixes stopped that day. To reach Joomla 5 you go through Joomla 4 first, and you cannot bring a Joomla 3 template with you. Templates written against the J3 system gave way to a new layout, child template, and media manager structure in J4. Most third-party templates from the 2014 to 2019 era never shipped a 4-compatible release. Ours was one of them.

The same problem hits extensions. The clinic site used K2 for service articles, a custom appointment-iframe wrapper, a SEF override, and an old gallery plugin. K2 only got a stable Joomla 4 build in late 2022. The SEF override was abandoned in 2017. The gallery plugin had an unpatched XSS that we had to mitigate at the web server.

So a "migration" in this case meant:

  • Upgrade J3.10 to its final patch level, then to J4, then to J5.
  • Rebuild the template against the J4 layout system.
  • Replace three extensions and refactor any code that depended on them.
  • Refactor custom PHP that touched JFactory, JRoute, and JText (renamed in J4).
  • Move the host to PHP 8.1 or 8.2 and fix every deprecation along the way.
  • Re-do this exercise in October 2025 when Joomla 4 itself goes end of life.

We costed it conservatively at 95 to 110 hours, plus a re-test of every page, plus the next forced upgrade cycle already on the horizon.

The static option on paper

The site had 124 pages: 4 location pages, 1 main services page with 14 service detail pages, 9 team profiles, an FAQ, a contact page, an EN mirror of all of the above, and roughly 70 blog posts going back to 2017.

Content updates happened maybe twice a month: a new team member, an opening-hours notice, a whitening promotion. The owner sometimes wrote a blog post. The practice manager wrote most of them.

Nothing on the site was personalised. The booking flow lived inside a third-party widget. Reviews came from a Google Places embed. The contact form posted to a CRM endpoint. The site was already static by behaviour. The Joomla install was running a database to render content that never changed between deploys.

We modelled the rebuild as an Astro 5 project with content collections, the existing design carried over to a fresh component library, and a small editing surface built on TinaCMS for the practice manager. Total estimate: 65 to 75 hours.

The numbers that decided it

We laid both paths next to each other in front of the dental group's CFO. Round numbers, no promises.

  • Joomla 5 migration: 95 to 110 dev hours, plus a J4 end-of-life cycle in October 2025, plus €52/month in managed VPS hosting, plus a monthly patch window.
  • Astro rebuild: 65 to 75 dev hours, no CMS runtime to patch, €0 to €20/month in static hosting, and no forced upgrade cycle until they choose one.

The migration would have shipped a site that looked the same and ran on the same architecture they had been quietly losing patience with for years. The rebuild cost less in hours, removed a class of recurring spend, and produced a faster site. The CFO signed off the same week.

Build week

Pulling content out of Joomla

We took a database dump and queried the content tables directly to extract everything to markdown.

SELECT id, title, alias, introtext, `fulltext`, language, publish_up
FROM jos_content
WHERE state = 1
ORDER BY publish_up DESC;

K2 articles lived in jos_k2_items, joined with jos_k2_categories for service grouping. A short Node script walked the rows, cleaned the HTML (TinyMCE leaves a decade of empty <p> tags and stray &nbsp;), downloaded images out of /images/stories/, and wrote one markdown file per page into a content collection.

Astro content collections

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const services = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    slug: z.string(),
    locale: z.enum(['nl', 'en']),
    summary: z.string().max(180),
    publishedAt: z.coerce.date(),
    cover: z.string().optional(),
  }),
});

export const collections = { services };

The same shape worked for team profiles and blog posts with different schemas. Translation pairs were linked by slug, so the language switcher could find the EN version of any NL page without a routing table.

Redirects and URL stability

The old Joomla SEF URLs used a mix of /component/k2/item/123-some-slug for service pages and a long tail of /index.php?option=com_content&view=article&id=45 fallbacks that had leaked into backlinks over the years. We mapped both into clean new paths via vercel.json and a static redirect manifest generated from the same Node script that exported the content. After two weeks live, Search Console reported under one percent 404 rate on indexed URLs and organic traffic to service pages was within four percent of the pre-launch baseline.

Booking iframe and contact form

The booking widget kept its iframe. The contact form became a Vercel serverless function that posted to the same CRM endpoint, with a Cloudflare Turnstile check on top. The Google Places review block was a tiny client component that hydrated on the locations page only.

Numbers after launch

The new site went live on 28 March 2026. We took a baseline of the old site the week before and measured the new one over the same connection profile.

  • Largest Contentful Paint on a mid-range Android over throttled 4G: 3.8s on Joomla, 0.9s on Astro.
  • Build-to-live for a content edit: 41 seconds via Vercel.
  • Hosting: from €52/month managed VPS with backups to €0 on the Vercel free tier (the site fits inside its bandwidth allotment with headroom).
  • Public attack surface: from one PHP runtime plus four extensions to a static CDN plus one form endpoint.

The thing that surprised the clinic owner most was the editing experience. The practice manager had avoided the Joomla backend for years. The new editor opens a side-by-side preview, lets her edit in place, and commits to git on save. She shipped 11 blog posts in the eight weeks after launch. The whole previous year, she shipped 4.

Takeaway

If a site already behaves like a static site, paying for a CMS migration is paying for runtime you don't use.

Cases where Joomla 5 still wins

We are not trying to overplay the case. Joomla 5 is the right answer when:

  • The site has real CMS behaviour: per-user content, ACL trees, member areas, forms with workflow.
  • A team of non-technical contributors already lives in Joomla and the cost of teaching them a new editor outweighs the migration cost.
  • A native extension is doing genuine work. A real shop on VirtueMart, a real community on EasySocial, a serious membership stack.
  • The custom code is small, the template is already J4-ready, and the third-party extensions all have current J5 releases.

For the Utrecht clinic group, none of those held. For most of the legacy Joomla sites we audit, none of those hold either. The CMS had been carrying static content for years, and the migration fee was the price of keeping it that way.

A five-minute audit before you book the migration

Before you sign a Joomla 5 quote, open the site and answer three questions honestly.

  • In the last 12 months, how many pages actually changed?
  • How many user accounts log in and do anything other than edit content?
  • How many of your installed extensions are doing work that a static site, a serverless function, and one third-party widget could not?

If the answers are "fewer than thirty", "zero", and "one or two", you are paying to maintain a runtime you don't use.

When we built the new site for the Utrecht dental group, the gnarliest part was not the Astro build. It was reading nine years of clinical content and deciding what to keep. We run that same audit before we quote any legacy migration, and most of the time it shapes the proposal more than the destination stack does.

Key takeaway

Before paying for a Joomla 5 migration, count what actually changed on the site in the last year. Under thirty pages and you are paying for a runtime you don't use.

FAQ

Will Astro work if our team edits content often?

Yes. With a git-backed editor like TinaCMS or Decap, a non-technical contributor edits in a live preview and saves to the repo. Builds redeploy in under a minute on Vercel or Netlify.

What happens to our existing Joomla SEO rankings?

You preserve them with a redirect manifest from old SEF URLs to new clean paths, generated from the same export script. Watch Search Console for the first two weeks and patch any tail 404s.

Can we keep our third-party booking widget?

Yes. Iframe widgets carry over unchanged. Anything that talked to a CRM or webhook becomes a small serverless function on the new host, usually a one-day port.

Is Joomla 5 always the wrong choice for a Joomla 3 site?

No. If the site has real member areas, ACL trees, native shop or community extensions, or a team already fluent in Joomla, a Joomla 5 migration is still the cheaper path.

joomlamigrationlegacy sitescase studyphparchitecture

Building something?

Start a project