diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 556c154010c76df6006417b1f358470e642cc771..103073b18cbb53eb3ee00efc19ac1441e30f8c4d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -35,9 +35,9 @@ include:
 
 variables:
   OPT_IN_TEST_CURRENT: 1
-  OPT_IN_TEST_MAX_PHP: 1
+  OPT_IN_TEST_MAX_PHP: 0
   # Broaden test coverage.
   OPT_IN_TEST_PREVIOUS_MAJOR: 1
   OPT_IN_TEST_PREVIOUS_MINOR: 0
-  OPT_IN_TEST_NEXT_MINOR: 1
-  OPT_IN_TEST_NEXT_MAJOR: 1
+  OPT_IN_TEST_NEXT_MINOR: 0
+  OPT_IN_TEST_NEXT_MAJOR: 0
diff --git a/src/Form/TermGlossaryConfigForm.php b/src/Form/TermGlossaryConfigForm.php
index a4ee31a066b30ba1742bfcade17682b2d587bf9f..89b5af8374d2ca8c5fba3f3432295ab4c9d9414b 100644
--- a/src/Form/TermGlossaryConfigForm.php
+++ b/src/Form/TermGlossaryConfigForm.php
@@ -38,7 +38,7 @@ class TermGlossaryConfigForm extends ConfigFormBase {
   /**
    * Plugin manager for term glossary handlers.
    *
-   * @var \Drupal\term_glossary\Service\TermGlossaryHandlerManager
+   * @var \Drupal\term_glossary\TermGlossaryHandlerInterface
    */
   protected $handlerManager;
 
@@ -287,11 +287,6 @@ class TermGlossaryConfigForm extends ConfigFormBase {
       $plugin = $this->handlerManager->createInstance($handler);
       $plugin->submitConfigurationForm($form, $form_state);
     }
-
-    // Invalidate glossary terms cache.
-    foreach ($vocabularies as $vocabulary) {
-      $this->glossaryManager->invalidateTermsCache($vocabulary);
-    }
   }
 
   /**
diff --git a/src/Service/TermGlossaryManager.php b/src/Service/TermGlossaryManager.php
index 00a9e7dc317ddb97dd5681280335e10e5e5f25f3..5788197c9ade2237aa5bc486eaeb939c3816bc44 100644
--- a/src/Service/TermGlossaryManager.php
+++ b/src/Service/TermGlossaryManager.php
@@ -5,7 +5,6 @@ namespace Drupal\term_glossary\Service;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
 use Drupal\Core\Cache\Context\CacheContextsManager;
 use Drupal\Core\Config\ConfigManagerInterface;
 use Drupal\Core\Config\ImmutableConfig;
@@ -82,7 +81,6 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
     protected LoggerChannelInterface $logger,
     protected TermGlossaryHandlerPluginManager $handlerManager,
     protected EntityRepositoryInterface $entityRepository,
-    protected CacheTagsInvalidatorInterface $cacheTagsInvalidator,
     protected CacheContextsManager $cacheContextsManager,
   ) {
     $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
@@ -111,6 +109,23 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
     return $this->handler;
   }
 
+  /**
+   * Escapes special characters for use in a regular expression character class.
+   *
+   * @param string $string
+   *   The input string to escape.
+   *
+   * @return string
+   *   The escaped string safe for use in a regex character class.
+   */
+  private static function escapeCharClass($string) {
+    return str_replace(
+      ['\\', '/', '-', ']', '[', '^'],
+      ['\\\\', '\/', '\-', '\]', '\[', '\^'],
+      $string
+    );
+  }
+
   /**
    * Builds the regexp pattern to match a term.
    */
@@ -135,13 +150,14 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
       $boundary_exceptions = $term_boundary_exceptions ?: $global_boundary_exceptions;
       if (empty($boundary_exceptions)) {
         // Use \b to match word boundaries.
-        $pattern = "/\b($quoted_name)\b/u";
+        $pattern = "/\b($quoted_name)\b/";
       }
       else {
         // Use negative lookbehind and lookahead to match word boundaries.
         // It allows avoiding matching punctuation characters like -:;.
-        $boundary = "(?:\p{L}|\p{N}|[" . $boundary_exceptions . "])";
-        $pattern = "/(?<!$boundary)($quoted_name)(?!$boundary)/u";
+        $escaped_boundary_exceptions = self::escapeCharClass($boundary_exceptions);
+        $boundary = "(?:\p{L}|\p{N}|[" . $escaped_boundary_exceptions . "])";
+        $pattern = "/(?<!$boundary)($quoted_name)(?!$boundary)/";
       }
     }
     else {
@@ -250,9 +266,11 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
         ? $root_entity->language()->getId()
         : $this->languageManager->getCurrentLanguage()->getId();
 
+      $cache_tags = [];
       $term_list = [];
       foreach ($vocabularies as $vocabulary) {
         $term_list += $this->getTerms($vocabulary, $langcode);
+        $cache_tags[] = "taxonomy_term_list:$vocabulary";
       }
 
       $is_single_match_per_field = $this->config->get('single_match') ?? FALSE;
@@ -356,11 +374,9 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
         }
       }
 
-      if ($ctx->tagIndex === 0) {
-        // No replacement has occurred, nothing to do.
-        return FALSE;
-      }
-      else {
+      $result = [];
+      $result['cache_tags'] = $cache_tags;
+      if ($ctx->tagIndex > 0) {
         // Replace all placeholders by their corresponding tags.
         $html = preg_replace_callback(
           '/\{\{(\d+)}}/',
@@ -379,8 +395,10 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
           },
           Html::serialize($html_dom),
         );
-        return ['html' => $html, 'count' => $ctx->tagIndex];
+        $result['html'] = $html;
+        $result['count'] = $ctx->tagIndex;
       }
+      return $result;
     }
   }
 
@@ -423,16 +441,8 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
   /**
    * {@inheritDoc}
    */
-  public function getConfigValue(string $key): ?string {
-    return $this->config->get($key);
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  public function invalidateTermsCache($vocabulary) {
-    $cache_tags = ["taxonomy_term_list:$vocabulary"];
-    $this->cacheTagsInvalidator->invalidateTags($cache_tags);
+  public function getConfig(): ?ImmutableConfig {
+    return $this->config;
   }
 
   /**
@@ -493,9 +503,9 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
     $entity_ids = $this->loadTermIds($vocabulary, $langcode);
     $terms = $this->termStorage->loadMultiple($entity_ids);
 
-    $per_term_options = $this->getConfigValue('per_term_options') ?? FALSE;
-    $term_synonyms = $this->getConfigValue('term_synonyms') ?? FALSE;
-    $synonyms_field = $this->getConfigValue('synonyms_field') ?? NULL;
+    $per_term_options = $this->config->get('per_term_options') ?? FALSE;
+    $term_synonyms = $this->config->get('term_synonyms') ?? FALSE;
+    $synonyms_field = $this->config->get('synonyms_field') ?? NULL;
     $has_synonyms = $term_synonyms && $synonyms_field != NULL;
     $terms_array = [];
     /**
@@ -549,7 +559,7 @@ class TermGlossaryManager implements TermGlossaryManagerInterface {
     $cid = $this->getCacheId($vocabulary, $langcode);
     // The cache will be invalidated when a term from this vocabulary is added,
     // updated, or deleted, due to the use of the following cache tag.
-    $cache_tags = ["taxonomy_term_list:$vocabulary"];
+    $cache_tags = array_merge($this->config->getCacheTags(), ["taxonomy_term_list:$vocabulary"]);
     $this->cache->set($cid, $terms_array, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
     return $terms_array;
   }
diff --git a/src/Service/TermGlossaryManagerInterface.php b/src/Service/TermGlossaryManagerInterface.php
index 8a56621a8e948cae3145021fbc8c77f5b034f139..f4021a3e9671eb53c1af75665dcbe67c31068479 100644
--- a/src/Service/TermGlossaryManagerInterface.php
+++ b/src/Service/TermGlossaryManagerInterface.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\term_glossary\Service;
 
+use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FormatterInterface;
 
@@ -34,15 +35,12 @@ interface TermGlossaryManagerInterface {
   public function attachLibrariesAndSettings(&$variables);
 
   /**
-   * Get configuration.
+   * Gets the module's immutable configuration object.
    *
-   * @param string $key
-   *   Config key.
-   *
-   * @return string|null
-   *   Config value.
+   * @return \Drupal\Core\Config\ImmutableConfig|null
+   *   The immutable configuration object or NULL if not available.
    */
-  public function getConfigValue(string $key): ?string;
+  public function getConfig(): ?ImmutableConfig;
 
   /**
    * Get vocabularies from preprocess variables array.
@@ -59,11 +57,6 @@ interface TermGlossaryManagerInterface {
    */
   public function getRootEntityFromFieldPreprocessVariables($variables);
 
-  /**
-   * Flush the glossary terms cache.
-   */
-  public function invalidateTermsCache($vocabulary);
-
   /**
    * Gets the configured term glossary handler.
    */
diff --git a/term_glossary.module b/term_glossary.module
index 2d3b87b08c8835275c1ede241d4b2a66c0567572..464c3af30f30776e7ed63e8f6416c3f9da37584f 100644
--- a/term_glossary.module
+++ b/term_glossary.module
@@ -8,6 +8,7 @@
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FormatterInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
@@ -119,12 +120,22 @@ function term_glossary_preprocess_field(&$variables) {
         $result = $glossaryManager->replaceFieldValue(
           $markup->jsonSerialize(), $vocabularies, $root_entity);
         if (is_array($result)) {
-          $replacements += $result['count'];
-          // Replace field markup content.
-          $variables['items'][$key]['content'] = [
-            '#type' => 'markup',
-            '#markup' => $result['html'],
-          ];
+          if (isset($result['html'])) {
+            $replacements += $result['count'];
+            // Replace field markup content.
+            $variables['items'][$key]['content'] = [
+              '#type' => 'markup',
+              '#markup' => $result['html'],
+            ];
+          }
+          // Attach cache tags to the render array.
+          $bubbleable_metadata = new BubbleableMetadata();
+          $cache_tags = $glossaryManager->getConfig()->getCacheTags();
+          if (!empty($result['cache_tags'])) {
+            $cache_tags = array_merge($cache_tags, $result['cache_tags']);
+          }
+          $bubbleable_metadata->setCacheTags($cache_tags);
+          $bubbleable_metadata->applyTo($variables);
         }
       }
       // Inject libraries and settings when replacements occurred.
diff --git a/term_glossary.services.yml b/term_glossary.services.yml
index 6e95230d99b96e8ead589105450bf6a7d0f1c7c0..62799af70291bbc9daacdbc3ce7ffbf609553e4b 100644
--- a/term_glossary.services.yml
+++ b/term_glossary.services.yml
@@ -19,5 +19,4 @@ services:
       - '@logger.channel.term_glossary'
       - '@plugin.manager.term_glossary.term_glossary_handler'
       - '@entity.repository'
-      - '@cache_tags.invalidator'
       - '@cache_contexts_manager'