Part 3 of a series. Start with Part 1 if you’re just joining.
Phase 3 is where the real work lives.
Phases 1 and 2 were about getting your environment ready — understanding the scope of the upgrade and making sure your infrastructure can support Drupal 11 when the time comes. Phase 3 is where you actually resolve every known blocker. The goal is to get to a state where upgrading Drupal core is the last remaining step, not the first.
For this project, Phase 3 had two distinct parts. The first was working through the module compatibility list — updating version constraints, investigating uncertain modules, removing things that no longer made sense. That part was relatively mechanical. The second part was the theme, and that was anything but.
The Module Audit
Working through the module list was mostly a process of visiting drupal.org project pages and updating composer.json constraints. For each module flagged by Upgrade Status, the questions were:
- Does a Drupal 11-compatible release exist?
- If it’s an alpha or RC, is it stable enough to run in production?
- If no compatible release exists, what’s the plan?
Most modules fell cleanly into category one. Commerce, Webform, Pathauto, Redis, Views Bulk Operations, Recaptcha, Sendgrid Integration — all had stable D11-compatible releases available. Updating their constraints in composer.json from something like ^2.0 to ^2.0 || ^3.0 (or just removing the upper bound) was enough to let Composer resolve them correctly once I was ready to run the update.
A few modules needed closer examination. drupal/scheduler had been on an RC release; by the time I was doing this work, a stable D11 release was available. drupal/google_analytics had been on an alpha; same situation. drupal/imagefield_slideshow and drupal/slick required more investigation — older media modules sometimes lag significantly on major version support.
The cleanest resolution for a module that doesn’t have D11 support is usually to remove it if it’s not critical, or find an actively maintained alternative. Leaving an incompatible module in place and hoping it works is a recipe for cryptic errors after the upgrade.
The Theme Decision
The module audit was work, but it was tractable work. The theme was a different kind of problem.
Four custom subthemes — aircooled, svloba, bootstrap_cis (for careersinspanish), and el_hincho — all extended drupal/bootstrap, the Bootstrap 3-based Drupal theme. There’s also a fifth site (the default/el_hincho site) using a theme in the same family.
The drupal/bootstrap module has not been updated for Drupal 11 and will not be. The project’s successor is drupal/bootstrap5, which is a completely separate module built on Bootstrap 5. These are not the same theme with a version bump — Bootstrap 3 and Bootstrap 5 have meaningfully different APIs, markup patterns, and component names. You can’t just swap the base theme declaration and expect things to work.
This meant the theme migration wasn’t optional, and it wasn’t small. Before I could upgrade Drupal core, I needed to:
- Install
drupal/bootstrap5 - Audit each custom subtheme and update its
.info.ymlto declarebootstrap5as the base theme - Identify any templates that used Bootstrap 3 markup and rewrite them for Bootstrap 5
- Audit all CSS for Bootstrap 3 class names that changed or were removed in Bootstrap 5
- Remap all block placements — Bootstrap 5 uses completely different region names than Bootstrap 3
Items 1 and 2 are straightforward. Items 3 through 5 are where the real work is, and the scope of that work varies significantly depending on how much custom template work your themes have.
Scoping the Work Per Subtheme
This is where reading your own code before making decisions pays off. I went through each subtheme and assessed what it actually contained.
Three of the four subthemes — aircooled, svloba, and the el_hincho default theme — were thin. They had a css/style.css with some custom overrides and a .info.yml, but no custom templates. The Bootstrap framework was doing most of the layout work. For these themes, the migration path was clear: update the .info.yml, update the CSS for any changed class names, remap block regions.
bootstrap_cis (used by careersinspanish) was different. It had a custom page.html.twig template that implemented a full Bootstrap 3 navbar — complete with data-toggle, data-target, navbar-default, and navbar-collapse markup that was specific to Bootstrap 3. That template needed a full rewrite.
There was also a legacy theme in the mix: business_responsive_theme_cis, a subtheme based on the contrib business_responsive_theme module. The careersinspanish subsite had been using this theme, and business_responsive_theme itself had its own set of patches and compatibility issues. Rather than trying to migrate a subtheme-of-a-contrib-theme forward, the cleaner move was to replace it entirely with bootstrap_cis — a clean Bootstrap 3 subtheme that I controlled — and then migrate bootstrap_cis to Bootstrap 5.
Making that decision first simplified everything that followed. Instead of tracking two different theme migration paths for careersinspanish, there was one: get the site onto bootstrap_cis, then migrate bootstrap_cis to Bootstrap 5 along with the other subthemes.
Before Touching Templates: Understand the Region Changes
The most disorienting part of moving from drupal/bootstrap to drupal/bootstrap5 for anyone who’s worked with the Bootstrap 3 theme is the region renaming. Bootstrap 3’s theme had regions named navigation and navigation_collapsible. Bootstrap 5’s theme does not have either of those — they’re replaced by nav_branding, nav_main, and nav_additional.
This matters because block placements in Drupal are stored in configuration with the region name. If you switch the base theme without remapping blocks, every block that was in a navigation region will be displaced — it won’t appear on the page at all, because the region it was assigned to no longer exists.
Making a mapping of old regions to new regions before you start moving blocks saves a lot of confusion. We’ll get into the specifics in the next post.
Expert Mode: Phase 3 Module Compatibility
Update contrib module constraints in composer.json:
For modules with stable D11 releases, update the version constraint to include ^11-compatible versions. The exact constraint depends on the module’s versioning scheme — check the project page for the current recommended release.
# Require updated versions explicitly (adjust versions as needed) ddev composer require \ "drupal/commerce:^2.38" \ "drupal/webform:^6.2" \ "drupal/pathauto:^1.12" \ "drupal/redis:^1.7" \ "drupal/views_bulk_operations:^4.3" \ "drupal/scheduler:^2.0" \ "drupal/google_analytics:^4.0" \ "drupal/recaptcha:^3.2" \ "drupal/address:^2.0"
Remove a module that has no D11 path:
# Uninstall from each subsite that uses it first for site in aircooled careersinspanish figueroa svloba; do ddev drush --uri=$site pm:uninstall module_name -y 2>/dev/null || true done # Then remove from the codebase ddev composer remove drupal/module_name
Check what’s actually enabled on each subsite:
for site in aircooled careersinspanish figueroa svloba; do echo "=== $site ===" ddev drush --uri=$site pm:list --status=enabled --format=table done
Install the Bootstrap 5 theme:
ddev composer require drupal/bootstrap5
Verify which theme each subsite is currently using:
for site in aircooled careersinspanish figueroa svloba; do echo "=== $site ===" ddev drush --uri=$site config:get system.theme done
Check your theme directory for what custom templates exist:
# See which themes have custom templates find web/themes/custom -name "*.html.twig" | sort # See what regions each custom theme declares grep -A 50 "regions:" web/themes/custom/*/THEMENAME.info.yml
The template inventory is important before you start making changes. You want to know exactly what you’re responsible for rewriting before you commit to the migration path. That inventory drives the work in Post 4.