On paper, a Contao upgrade is a Composer command and a database migration. The command is the easy part. The real work is making sure the site looks and behaves exactly as it did the day before, on every page, on desktop and on phones.
We have taken several sites from Contao 4.13 and 5.3 to 5.7, and the same few problems turn up nearly every time. I keep a list of them now, with the fixes and the order we do things in, so they get caught long before anything reaches the client's inbox.
Modernized elements print new classes
Contao 5 rebuilt many content elements and front-end modules on Twig templates. Contao calls these "modernized" elements. They print different wrapper classes and, in some cases, different inner markup. Your stylesheet was written against the old markup, so rules that worked for years quietly stop matching.
A simple test tells you which selectors changed. If the element has a Twig template in the core bundle (content_element/text.html.twig, for example), its class changed. If it only has a legacy template, it didn't.
.ce_textbecomes.content-text,.ce_imagebecomes.content-image, and so on.- Modernized modules go from
mod_*tomodule-*. - Underscores turn into hyphens, so
ce_unfiltered_htmlbecomescontent-unfiltered-html. - Some things keep their old classes: the form element (
.ce_form), the navigation modules,.mod_article, and third-party modules such as Isotope or FAQ lists. Adding new class names for these matches nothing.
The lost .block overflow
This one catches almost everyone. Contao 4.13 added a block class to every content element, and most themes carry .block { overflow: hidden }. Few people noticed that the rule was also containing floated images inside text elements. Modernized elements in 5.7 don't get the block class, so floats start spilling into the next element and layouts shift.
The fix is to give the new wrappers their own block formatting context. We use display: flow-root rather than overflow: hidden, because it contains floats without clipping focus rings, shadows or tooltips:
.block,
[class^="content-"], [class*=" content-"],
[class^="module-"], [class*=" module-"] {
display: flow-root;
}
Inner markup changed too
Fixing the wrapper names only gets you partway. Inside the element, a lot of the markup is different:
.image_containeris now a bare<figure>..captionis now a plain<figcaption>with no class.- Gallery column classes like
cols_4moved to a modifier on the wrapper:.content-gallery--cols-4. - Text elements with an image use
.media--left,.media--rightand friends. - Download lists get new hooks such as
.download-elementand.ext-pdf.
These break even after the wrapper classes are fixed, which is why a quick find-and-replace never finishes the job.
Old and new selectors side by side
We never replace the old selector. We add the new one beside it:
.ce_text,
.content-text { margin-bottom: 2rem; }
.ce_image .image_container,
.content-image.content-image figure { border-radius: 8px; }
This keeps the old staging site working while the new one is built, so we can compare them side by side. Two details matter. First, the new selector must be at least as specific as the old one, or another rule wins; repeating the wrapper class (.content-image.content-image) is a simple way to match weight. Second, when you upgrade from 5.3 rather than 4.13, many elements already printed the new classes, so their old .ce_* rules were dead on the live site. Partnering a dead rule changes what visitors see. Only partner what was actually live, and check it against a capture of the old pages.
Templates move to Twig
In 5.7, a theme's .html.twig template wins over an .html5 template of the same name, and Contao 6 drops .html5 entirely. So every live template override needs converting, and every dead one should go.
Deciding which overrides are live is harder than it sounds. Don't search the old pages for the template's class name, because many overrides hard-code their own wrapper. Look for something only that template outputs. A lot of overrides turn out to be old copies of core templates with no real change; diff them against the 5.7 core and delete the ones that add nothing. Old fe_page copies deserve special attention. They often miss the canonical link and the structured-data scripts that newer versions add, so extend the core template instead of copying it.
Extensions and Isotope
Before touching anything, list every Composer requirement and check whether it has a 5.7-compatible release. Then check whether each one is actually used. On one upgrade, two extensions that looked like they needed a hand port were not used anywhere, so we removed them.
Isotope eCommerce sites need extra care. Moving from Isotope 2 to 3, the database migration can stop with an error like Missing attribute class for field "spec_sheet". Isotope 3 core only registers a short list of attribute types. File and document attributes still carry their Isotope 2 types, which now live in a separate file-attribute bundle. Contao reads every attribute on boot, so one unconverted attribute stops the migration before it can convert anything, and you end up going in circles.
Install order fixes it: require the compatibility bundles first, then migrate. On one site the bundle was installed but a stale composer.lock pinned an older commit without the fix, and the loop came back. That is why we keep a fixes readme in the repo. Every error goes in with its cause and its fix, so the next site takes minutes instead of a day.
Our order of work
- Inventory everything: packages, extensions actually in use, template overrides, redirect tables, anything odd in the web root.
- Copy the production files and database into a local DDEV environment. Boot it unchanged first, confirm it matches the live site, and take a snapshot.
- Capture the HTML and a screenshot of every published page as a baseline, logged in where pages are protected.
- Pin
5.7.*, update, and migrate in steps, with a snapshot before each one. - Convert templates and partner selectors, re-capturing after each change.
- Diff every page visually, old against new, at 1440px and 390px. The target is no difference beyond the changes we made on purpose.
- Build a fresh staging install from a clean bundle (no vendor folder, no old server
.htaccess), with PHP 8.4 for web and command line and the caches purged. - Take a full backup, then point the live address at the new install, out of hours.
The migration step runs through the console:
php vendor/bin/contao-console contao:migrate --dry-run
php vendor/bin/contao-console contao:migrate --no-backup
php vendor/bin/contao-console contao:migrate --with-deletes
php vendor/bin/contao-console contao:automator purgeScriptCache
The cache purge on the last line matters more than it looks. Compiled theme SCSS isn't rebuilt on its own, and stale CSS looks exactly like a fix that didn't work.
One recent upgrade
For a project we did with Bright Cloud Studio, Art Material Essentials went from Contao 5.3.39 to 5.7.13 on PHP 8.4. Every template was converted to Twig, and we confirmed the CSS was identical page by page on desktop and mobile. Along the way we fixed problems the old site already had: broken images, missing meta descriptions, and .html URLs, which now redirect to clean addresses. It went to a fresh staging install first, then live.
If your site is still on 4.13 or 5.3, send us the address and we'll tell you what the upgrade involves.