This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.


This section provides information for developers who wish to help collaborate and improve Drupal WxT.

1 - Architecture

The goal of Drupal WxT since the 4.1.x line is to make the installation profile very minimal by default but providing additional extensions that can be enabled as desired.

What WxT offers is some light enhancements to Drupal Core, mainly around security and performance, and integration with the Web Experience Toolkit. By default, the distribution offers minimal functionality to allow full customizations by users. However a great deal of optional extensions are available that can provide additional functionality generally beneficial to Government departments.

Note: In the future we are looking into providing a list of community modules that are build to work with the distribution but are “out of tree”.

All of the optional modules are located in the wxt_ext folder named after WxT Extend and can be enabled during the initial site installation by passing the following flag via the drush cli:


Note: If you wish to only install the minimum set of dependencies please remove the wxt_extension_configure_form.select_all='TRUE' flag in its entirety.

In order to provide a list of the optional enabled extensions during the installation that can be checked, all that any module has to do is provide a modulename.wxt_extension.yml file in their root and they will be picked as installable during the profile install and also respond to the additional drush flag discussed above.

For more information on some of the history leading to this design:

2 - Composer

We highly recommend using our Composer Project Template to build and maintain your WxT derived project’s codebase.

Getting Started

The following command is all you need to get started:

composer create-project drupalwxt/wxt-project:4.3.4 <site-name>

Note: For development you may also specify a branch using drupalwxt/wxt-project:4.3.x-dev.

You can see a working example of a fully generated Composer Project Template over at:

Where the following is the command that was used for the initial generation:

composer create-project drupalwxt/wxt-project:4.3.4 site-wxt

Note: Remember to keep the composer.json and composer.lock files that exist above docroot in source control as they are controlling your dependencies.


List of common commands are as follows:

Task Composer
Installing a contrib project (latest version) composer require drupal/PROJECT
Installing a contrib project (specific version) composer require drupal/PROJECT:1.0.0-beta5
Updating all projects including Drupal Core composer update
Updating a single contrib project composer update drupal/PROJECT_NAME
Updating Drupal Core composer update drupal/core

Note: Composer is a dependency manager and helps us keep track of what code and at what version our application relies on so that it always get installed the right way on every copy of that application.

Specifying a version

A specific version can be specified from the cli:

composer require drupal/<modulename>:<version>

However please note if you specify a branch, such as 1.x you must add -dev to the end of the version:

composer require drupal/token:1.x-dev

Source Control

Taking a look at the .gitignore file, you will discover that certain directories, including all those directories containing contributed projects, are excluded from source control which is by design.

Note: Unlike Drush in a Composer derived project you should never commit your install dependencies to source control.

Composer will create composer.lock file, which is a list of dependencies that were installed, and in which versions.

Note: In general you should always commit your composer.lock file to source control so that others via a quick composer install can have everything installed along with the correct versions specified in the composer.lock file.

How to update Drupal Core?

Please don’t add drupal/core to your project’s composer.json since WxT manages Drupal Core for you along with the series of patches on top of it.

WxT’s minor versions will always correspond to Drupal Core’s. For example, drupalwxt/wxt:~4.4.x will require Drupal Core 9.4.x and drupalwxt/wxt:~4.3.x required Drupal Core 9.3.x.

When you need to update Drupal Core as an example from 9.3.x to 9.4.x, all you would do is change your requirement for drupalwxt/wxt in your composer.json file:

composer require --no-update drupalwxt/wxt:~4.4.0
composer update

Compatibility table

drupalwxt/wxt version Drupal Core version Drush version
~4.4.x 9.4.x >=9.7
~4.3.x 9.3.x >=9.7
~4.2.x 9.2.x >=9.7
~4.1.x 9.1.x >=9.7
~4.0.x 8.8.x+ >=9.7

3 - Configuration Management

Drupal WxT thanks to the work done by the Acquia Team is able to use advanced configuration management strategies.

At the moment this remains an opt-in process and you will have to add the following modules to your composer.json before you add the code snippet below to your settings.php file.

Once enabled all default configuration will be stored in /sites/default/files/config/default/ and then depending on your environment additionally configuration splits can be leveraged depending on your SDLC.

 * Configuration Split for Configuration Management
 * WxT is following the best practices given by Acquia for configuration
 * management. The "default" configuration directory should be shared between
 * all multi-sites, and each multisite will override this selectively using
 * configuration splits.
 * To disable this functionality simply set the following parameters:
 * $wxt_override_config_dirs = FALSE;
 * $settings['config_sync_directory'] = $dir . "/config/$site_dir";
 * See
 * for more information.

use Drupal\wxt\Robo\Common\EnvironmentDetector;

if (!isset($wxt_override_config_dirs)) {
  $wxt_override_config_dirs = TRUE;
if ($wxt_override_config_dirs) {
  $config_directories['sync'] = $repo_root . "/var/www/html/sites/default/files/config/default";
  $settings['config_sync_directory'] = $repo_root . "/var/www/html/sites/default/files/config/default";
$split_filename_prefix = 'config_split.config_split';
if (isset($config_directories['sync'])) {
  $split_filepath_prefix = $config_directories['sync'] . '/' . $split_filename_prefix;
else {
  $split_filepath_prefix = $settings['config_sync_directory'] . '/' . $split_filename_prefix;

 * Set environment splits.
$split_envs = [
foreach ($split_envs as $split_env) {
  $config["$split_filename_prefix.$split_env"]['status'] = FALSE;
if (!isset($split)) {
  $split = 'none';
  if (EnvironmentDetector::isLocalEnv()) {
    $split = 'local';
  if (EnvironmentDetector::isCiEnv()) {
    $split = 'ci';
  if (EnvironmentDetector::isDevEnv()) {
    $split = 'dev';
  elseif (EnvironmentDetector::isTestEnv()) {
    $split = 'test';
  elseif (EnvironmentDetector::isQaEnv()) {
    $split = 'qa';
  elseif (EnvironmentDetector::isProdEnv()) {
    $split = 'prod';
if ($split != 'none') {
  $config["$split_filename_prefix.$split"]['status'] = TRUE;

 * Set multisite split.
// $config["$split_filename_prefix.SITENAME"]['status'] = TRUE;

4 - Performance

Below are some recommended settings that improve the performance of Drupal WxT sites.

4.1 - PostgreSQL

To properly configure PostgreSQL with Drupal you should ensure the following configuration is used.

Note: Some customizations might be necessary depending on your individual requirements.

  listenAddresses: "'*'"
  maxConnections: "200"
  sharedBuffers: 512MB
  workMem: 2048MB
  effectiveCacheSize: 512MB
  effectiveIoConcurrency: "100"
  maintenanceWorkMem: 32MB
  minWalSize: 512MB
  maxWalSize: 512MB
  walBuffers: 8048kB
  byteaOutput: "'escape'"
  hugePages: "off"
  walLevel: "replica"
  maxWalSenders: "0"
  synchronousCommit: "on"
  walKeepSegments: "130"
  checkpointTimeout: "'15 min'"
  checkpointCompletionTarget: "0.9"
  walCompression: "on"
  walWriterDelay: 200ms
  walWriterFlushAfter: 1MB
  bgwriterDelay: 200ms
  bgwriterLruMaxpages: "100"
  bgwriterLruMultiplier: "2.0"
  bgwriterFlushAfter: "0"
  maxWorkerProcesses: "8"
  maxParallelWorkersPerGather: "4"
  maxParallelWorkers: "4"

Note: The above is written in yaml syntax which will work for both Docker Compose and Kubernetes Helm Charts. For the postgresql.conf file itself without using these tools simply find the _ counterpart.

Queries leveraging ILIKE

There is a known PostgreSQL performance issue that exists in Drupal and is related to leveraging queries with ILIKE.

This issue is particularly noticeable in relation to the path_alias table.

There are patches being worked on to handle this in Drupal core but a very quick fix can be implemented leveraging pg_trgm.

There is a great blog article listed below which goes over this issue in more detail.

The instructions are a bit outdated so the updated syntax to enter in psql is given below:

CREATE INDEX path_alias__alias_trgm_gist_idx ON path_alias USING gist (alias gist_trgm_ops);
CREATE INDEX path_alias__path_trgm_gist_idx ON path_alias USING gist (path gist_trgm_ops);
ANALYZE path_alias;

4.2 - Redis

To properly configure Redis with Drupal you should ensure the following configuration is added to your settings.php file.

Note: Some customizations might be necessary depending on your individual requirements.

if (extension_loaded('redis')) {
  // Set Redis as the default backend for any cache bin not otherwise specified.
  $settings['cache']['default'] = 'cache.backend.redis';
  $settings['redis.connection']['interface'] = 'PhpRedis';
  $settings['redis.connection']['scheme'] = 'http';
  $settings['redis.connection']['host'] = 'localhost';
  $settings['redis.connection']['port'] = '6379';
  $settings['redis.connection']['password'] = getenv('REDIS_PASSWORD') ?: '';
  $settings['redis.connection']['persistent'] = FALSE;

  // Allow the services to work before the Redis module itself is enabled.
  $settings['container_yamls'][] = 'modules/contrib/redis/';
  $settings['container_yamls'][] = 'modules/contrib/redis/';

  // Manually add the classloader path, this is required for the container cache bin definition below
  // and allows to use it without the redis module being enabled.
  $class_loader->addPsr4('Drupal\\redis\\', 'modules/contrib/redis/src');

  $settings['bootstrap_container_definition'] = [
    'parameters' => [],
    'services' => [
      'redis.factory' => [
        'class' => 'Drupal\redis\ClientFactory',
      'cache.backend.redis' => [
        'class' => 'Drupal\redis\Cache\CacheBackendFactory',
        'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
      'cache.container' => [
        'class' => '\Drupal\redis\Cache\PhpRedis',
        'factory' => ['@cache.backend.redis', 'get'],
        'arguments' => ['container'],
      'cache_tags_provider.container' => [
        'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
        'arguments' => ['@redis.factory'],
      'serialization.phpserialize' => [
        'class' => 'Drupal\Component\Serialization\PhpSerialize',

  /** Optional prefix for cache entries */
  $settings['cache_prefix'] = 'drupal_';

  // Always set the fast backend for bootstrap, discover and config, otherwise
  // this gets lost when redis is enabled.
  $settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
  $settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
  $settings['cache']['bins']['config'] = 'cache.backend.chainedfast';

  // Use for all bins otherwise specified.
  $settings['cache']['default'] = 'cache.backend.redis';

  // Use for all queues unless otherwise specified for a specific queue.
  $settings['queue_default'] = 'queue.redis';

  // Or if you want to use reliable queue implementation.
  // $settings['queue_default'] = 'queue.redis_reliable';

  // Use this to only use Redis for a specific queue.
  // $settings['queue_service_aggregator_feeds'] = 'queue.redis';

  // Use this to use reliable queue implementation.
  // $settings['queue_service_aggregator_feeds'] = 'queue.redis_reliable';

4.3 - Varnish

To properly configure Varnish with Drupal you should ensure the following configuration is your default.vcl file.

Note: Some customizations might be necessary depending on your individual requirements.

vcl 4.0;

import std;
import directors;

backend nginx {
  .host = "hostname-nginx";
  .host_header = "hostname-nginx";
  .port = "80";

sub vcl_init {
  new backends = directors.round_robin();

sub vcl_recv {
  set req.http.X-Forwarded-Host = req.http.Host;
  if (!req.http.X-Forwarded-Proto) {
    set req.http.X-Forwarded-Proto = "http";

  # Answer healthcheck
  if (req.url == "/_healthcheck" || req.url == "/healthcheck.txt") {
    return (synth(700, "HEALTHCHECK"));
  set req.backend_hint = backends.backend();

  # Answer healthcheck
  if (req.url == "/_healthcheck" || req.url == "/healthcheck.txt") {
    return (synth(700, "HEALTHCHECK"));
  set req.backend_hint = backends.backend();

  # Always cache certain file types
  # Remove cookies that Drupal doesn't care about
  if (req.url ~ "(?i)\.(asc|dat|tgz|png|gif|jpeg|jpg|ico|swf|css|js)(\?.*)?$") {
    unset req.http.Cookie;
  } else if (req.http.Cookie) {
    set req.http.Cookie = ";" + req.http.Cookie;
    set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
    set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|SSESS[a-z0-9]+|NO_CACHE)=", "; \1=");
    set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
    if (req.http.Cookie == "") {
        unset req.http.Cookie;
    } else {
        return (pass);
  # If POST, PUT or DELETE, then don't cache
  if (req.method == "POST" || req.method == "PUT" || req.method == "DELETE") {
    return (pass);
  # Happens before we check if we have this in cache already.
  # Typically you clean up the request here, removing cookies you don't need,
  # rewriting the request, etc.
  return (hash);
  #return (pass);

sub vcl_backend_fetch {
  # NEW
  set bereq.http.Host = "hostname-nginx";

  # Don't add to X-Forwarded-For
  set bereq.http.X-Forwarded-For = regsub(bereq.http.X-Forwarded-For, "(, )?127\.0\.0\.1$", "");

sub vcl_backend_response {
  if (beresp.http.Location) {
    set beresp.http.Location = regsub(
      bereq.http.X-Forwarded-Proto + "://" + bereq.http.X-Forwarded-Host + "/"
  # Only cache select response codes
  if (beresp.status == 200 || beresp.status == 203 || beresp.status == 204 || beresp.status == 206 || beresp.status == 300 || beresp.status == 301 || beresp.status == 404 || beresp.status == 405 || beresp.status == 410 || beresp.status == 414 || beresp.status == 501) {
    # Cache for 5 minutes
    set beresp.ttl = 5m;
    set beresp.grace = 12h;
    set beresp.keep = 24h;
  } else {
    set beresp.ttl = 0s;

sub vcl_deliver {
  # Remove identifying information
  unset resp.http.Server;
  unset resp.http.X-Powered-By;
  unset resp.http.X-Varnish;
  unset resp.http.Via;

  # Comment these for easier Drupal cache tag debugging in development.
  unset resp.http.Cache-Tags;
  unset resp.http.X-Drupal-Cache-Contexts;

  # Add Content-Security-Policy
  # set resp.http.Content-Security-Policy = "default-src 'self' * *; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval' *  * blob:; connect-src 'self' * * * *; img-src 'self' * * * * data:; font-src 'self' *";

  # Add CORS Headers
  # if (req.http.Origin ~ "(?i)\.example\.ca$") {
  #   if (req.url ~ "\.(ttd|woff|woff2)(\?.*)?$") {
  #     set resp.http.Access-Control-Allow-Origin = "*";
  #     set resp.http.Access-Control-Allow-Methods = "GET";
  #   }
  # }

  # Add X-Frame-Options
  if (req.url ~ "^/livechat" || req.url ~ "^/(en/|fr/)?entity-browser/") {
    set resp.http.X-Frame-Options = "SAMEORIGIN";
  } else {
    set resp.http.X-Frame-Options = "DENY";

  set resp.http.X-Content-Type-Options = "nosniff";
  set resp.http.X-XSS-Protection = "1; mode=block";

  # Happens when we have all the pieces we need, and are about to send the
  # response to the client.
  # You can do accounting or modifying the final object here.
  if (obj.hits > 0) {
    set resp.http.X-Cache = "HIT";
  } else {
    set resp.http.X-Cache = "MISS";
  # Handle errors
  if ( (resp.status >= 500 && resp.status <= 599)
    || resp.status == 400
    || resp.status == 401
    || resp.status == 403
    || resp.status == 404) {
    return (synth(resp.status));

sub vcl_synth {
  # Remove identifying information
  unset resp.http.Server;
  unset resp.http.X-Powered-By;
  unset resp.http.X-Varnish;
  unset resp.http.Via;

  # Add Content-Security-Policy
  # set resp.http.Content-Security-Policy = "default-src 'self' *; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval' * * blob:; connect-src 'self' * * * *; img-src 'self' * data:;";
  # set resp.http.X-Content-Type-Options = "nosniff";
  # set resp.http.X-Frame-Options = "DENY";
  # set resp.http.X-XSS-Protection = "1; mode=block";

  # if (resp.status >= 500 && resp.status <= 599) {
  #   set resp.http.Content-Type = "text/html; charset=utf-8";
  #   synthetic(std.fileread("/data/configuration/varnish/errors/503.html"));
  #   return (deliver);
  # } elseif (resp.status == 400) { # 400 - Bad Request
  #   set resp.http.Content-Type = "text/html; charset=utf-8";
  #   synthetic(std.fileread("/data/configuration/varnish/errors/400.html"));
  #   return (deliver);
  # } elseif (resp.status == 401) { # 401 - Unauthorized
  #   set resp.http.Content-Type = "text/html; charset=utf-8";
  #   synthetic(std.fileread("/data/configuration/varnish/errors/401.html"));
  #   return (deliver);
  # } elseif (resp.status == 403) { # 403 - Forbidden
  #   set resp.http.Content-Type = "text/html; charset=utf-8";
  #   synthetic(std.fileread("/data/configuration/varnish/errors/403.html"));
  #   return (deliver);
  # } elseif (resp.status == 404) { # 404 - Not Found
  #   set resp.http.Content-Type = "text/html; charset=utf-8";
  #   synthetic(std.fileread("/data/configuration/varnish/errors/404.html"));
  #   return (deliver);
  # } else
  if (resp.status == 700) { # Respond to healthcheck
    set resp.status = 200;
    set resp.http.Content-Type = "text/plain";
    synthetic ( {"OK"} );
    return (deliver);

# sub vcl_backend_error {
#   set beresp.http.Content-Type = "text/html; charset=utf-8";
#   synthetic(std.fileread("/data/configuration/varnish/errors/503.html"));
#   return (deliver);
# }

5 - Release Process

Select a version number

WxT releases are numbered using a form of semantic versioning. More information can be found in our Versioning page.


In general, when preparing a release: increment the FEATURE when Drupal Core has a major release (ie. 9.3.x to 9.4.x) otherwise simply increment the SPRINT number.

Create an issue on

Create an issue in the Drupal WxT project on GitHub for release tracking, title it Release x.x.x (where x.x.x is the incremented version number).

This issue should contain the following checklist as well as any other related steps or information regarding preparing the release.

See the [full release documentation]( for more detail.

- [ ] All related projects (wxt_library and wxt_bootstrap) tagged and released on and
- [ ] Version number selected
- [ ] updated
- [ ] composer.json updated
- [ ] Run for hook_updates and wxt contrib
- [ ] CI build passes
- [ ] Releases tagged and pushed to and
- [ ] WxT released on (
- [ ] Add changelog information to published tag once CI is done

Update changelog

Ensure the changelog contains an entry for the release and is updated as issues and changes are resolved (in the next steps or when committing code / changes).

Review dependent wxt modules

  1. Review contrib modules in composer.json (or in a site install; extend->update) and update as necessary.
  2. If necessary, tag wxt_library and update wxt’s composer.json file
  3. If necessary, tag wxt_bootstrap and update wxt’s composer.json file

All projects must be released on (and github).

Note: Changes to composer.json file (specifically dev dependencies and repositories) should be mentioned in the CHANGELOG. version does not currently support semantic versioning. Instead, the version number on is 8.x-X.YZZ, where:

  • X = MAJOR
  • ZZ = SPRINT (two digits - add leading zero for < 10)

Check composer.json

If the wxt dependent modules are updated, we need to reflect this in wxt composer.json and the CHANGELOG.

  • git clone
  • Confirm or update that it’s using appropriate tags of wxt_library + wxt_bootstrap (composer.json)
  • Push to and repositories any changes
  • Ensure GitHub Actions build passes

Tag WxT and



  1. Go to Tags page
  2. Click … and select create release on the tag
  3. Enter the version number in the release title
  4. Copy the changelog entry for this release into the release notes
  5. Click publish release

The builds on are incomplete as they don’t fully support Composer yet which is why we host a tarball on GitHub for those not using Composer.

  1. Drupal WxT Release Page
  2. Select the tag
  3. Copy/paste the following blurb into the release notes:

See the <a href=""></a> file.

6 - Theming

Largely when doing any theme related work with Drupal WxT this almost always should be done in a sub-theme.

For more on creating sub-themes please consult the official documentation:

To assist with sub-theme creation WxT Bootstrap provides an example starterkit that should be of benefit.

Note: Sub-themes are just like any other theme except they inherit the parent theme’s resources.

Sub Theme Configuration

a) Replace every instance of THEMENAME with your chosen machine name often of the pattern <prefix>_bootstrap.

b) Enable your new sub-theme preferably via drush:

drush en `<prefix>_bootstrap`
drush cc css-js

c) Point to your new sub theme for WxT Library to properly load assets under Themes Visibility on the /admin/config/wxt/wxt_library page.


Inheriting Block Templates

If the theme you are extending has custom block templates these won’t be immediately inherited because a sub-theme creates copies of all the blocks in the parent theme and renames them with the sub-theme’s name as a prefix. Twig block templates are derived from the block’s name, so this breaks the link between these templates and their block.

Fixing this problem currently requires a hook in the THEMENAME.theme file and should have the following contents:

 * Implements hook_theme_suggestions_HOOK_alter().
function THEMENAME_theme_suggestions_block_alter(&$suggestions, $variables) {
  // Load theme suggestions for blocks from parent theme.
  for ($i = 0; $i < count($suggestions); $i++) {
    if (str_contains($suggestions[$i], 'THEMENAME_')) {
      $new_suggestions = [
        str_replace('THEMENAME_', '', $suggestions[$i]),
        str_replace('THEMENAME_', 'wxt_bootstrap_', $suggestions[$i]),
      array_splice($suggestions, $i, 0, $new_suggestions);
      $i += 2;

Programmatic Logic

The following provides an example of how you can configure your sub theme to be installed as the default on a module install:

 * Implements hook_modules_installed().
function MODULENAME_modules_installed($modules) {
    if (in_array('wxt', $modules)) {
        ->set('default', 'THEMENAME')
        ->set('admin', 'claro')

The following provides an example of how you can configure wxt_library to use your sub theme by creating a config/install/wxt_library.settings.yml file with the following contents:

  visibility: 0
    - 'admin*'
    - 'imagebrowser*'
    - 'img_assist*'
    - 'imce*'
    - 'node/add/*'
    - 'node/*/edit'
    - 'print/*'
    - 'printpdf/*'
    - 'system/ajax'
    - 'system/ajax/*'
  visibility: 1
    wxt_bootstrap: wxt_bootstrap
  options: 1
    css: css
    js: js
  theme: theme-gcweb

7 - Versioning

The Drupal WxT distribution is following semantic versioning.

WxT typically makes a sprint release every four to six weeks. We will also use sprint releases to package new minor releases of Drupal Core with WxT as they become available.

In addition, we will also increment the major version number of WxT about once every four to six months.


Support for semantic versioning for extensions (modules, themes, etc) is still ongoing.

The three parts of our versioning system are MAJOR.FEATURE.SPRINT.

Given the following tag: 9.x-2.15:

9 Major version of Drupal Core
2 Major version of WxT
1 Feature release of WxT. Also increments with minor core releases.
5 Sprint release between feature releases

Note: Due to the constraints of, there is no separator between the FEATURE and SPRINT digits.