diff --git a/composer.json b/composer.json
index 30169182527e49e5bdad4783101e7ac60811dcc2..0ba6b5842e386678fdfb0a95aeb984b905100b9f 100644
--- a/composer.json
+++ b/composer.json
@@ -126,7 +126,7 @@
         "drupal/inline_entity_form": "1.0-rc15",
         "drupal/libraries": "4.0",
         "drupal/link_attributes": "1.12",
-        "drupal/linkit": "5.0-beta13",
+        "drupal/linkit": "6.0.0",
         "drupal/mathjax": "4.0.2",
         "drupal/media_entity_browser": "2.0-alpha4",
         "drupal/media_entity_file_replace": "^1.0",
@@ -286,9 +286,6 @@
             "drupal/honeypot": {
                 "2811189": "https://www.drupal.org/files/issues/2022-05-25/honeypot-field_weight-2811189-27_0.patch"
             },
-            "drupal/linkit": {
-                "2712951": "https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch"
-            },
             "drupal/social_media_links": {
                 "Remove Google Plus": "patches/rm-googleplus.patch",
                 "Fix Empty Link": "patches/accessibility-fix-empty-link.patch"
diff --git a/composer.lock b/composer.lock
index c99c2f904ce1c1a44095252d0203140485540258..f83787ef354ccc83a370354caaaf17cf807ff1f9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "5e44a8642c1a018223a53f36ef6178a1",
+    "content-hash": "29697f1f20312fa7ad5e6e7db1e7e4e5",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -5098,32 +5098,36 @@
         },
         {
             "name": "drupal/linkit",
-            "version": "5.0.0-beta13",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/linkit.git",
-                "reference": "8.x-5.0-beta13"
+                "reference": "6.0.0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/linkit-8.x-5.0-beta13.zip",
-                "reference": "8.x-5.0-beta13",
-                "shasum": "9215fbea84166cabc9b7a2d9a04dedaffb9fc1ed"
+                "url": "https://ftp.drupal.org/files/projects/linkit-6.0.0.zip",
+                "reference": "6.0.0",
+                "shasum": "3c4143eb797abee04e5af47eb1885a65e6321b51"
             },
             "require": {
-                "drupal/core": "^8.7.7 || ^9"
+                "drupal/core": "^9.4 || ^10.0.0"
+            },
+            "conflict": {
+                "drupal/core": ">=10.1"
             },
             "require-dev": {
+                "drupal/ckeditor": "*",
                 "drupal/imce": "*"
             },
             "type": "drupal-module",
             "extra": {
                 "drupal": {
-                    "version": "8.x-5.0-beta13",
-                    "datestamp": "1632946970",
+                    "version": "6.0.0",
+                    "datestamp": "1688748025",
                     "security-coverage": {
-                        "status": "not-covered",
-                        "message": "Beta releases are not covered by Drupal security advisories."
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
                     }
                 }
             },
diff --git a/scripts/tmux-parallel-push.sh b/scripts/tmux-parallel-push.sh
index df7193fb7e4a180cc165fb0803656f5778a0e985..ee661eab2bcd375134ab86b17ae982dbdb5e1baa 100755
--- a/scripts/tmux-parallel-push.sh
+++ b/scripts/tmux-parallel-push.sh
@@ -1,66 +1,79 @@
 #!/usr/bin/env bash
 
-if [[ -z `which parallel` || -z `which tmux` || -z `which gdate` ]]; then
-  echo "Either 'parallel' or 'tmux' is not installed...";
-  if [[ -z `which brew` ]]; then
-    echo "Homebrew is NOT installed. Homebrew needs to be installed to proceed.";
-    echo "Visit https://https://brew.sh/ for more information.";
-    exit 1;
+if [[ -z $(which parallel) || -z $(which tmux) || -z $(which gdate) ]]; then
+  echo "Either 'parallel' or 'tmux' is not installed..."
+  if [[ -z $(which brew) ]]; then
+    echo "Homebrew is NOT installed. Homebrew needs to be installed to proceed."
+    echo "Visit https://https://brew.sh/ for more information."
+    exit 1
   else
-    echo "Attempting to install parallel, tmux, coreutils...";
-    brew install parallel tmux coreutils;
+    echo "Attempting to install parallel, tmux, coreutils..."
+    brew install parallel tmux coreutils
   fi
 fi
 
 if [[ -z $1 ]]; then
-  echo; echo "Usage: $0 <env> [Deploy message]";
-  echo; echo;
-  exit 1;
+  echo
+  echo "Usage: $0 <env> [Deploy message]"
+  echo
+  echo
+  exit 1
 fi
 
-ENV=$1;
-shift;
+ENV=$1
+shift
 
 if [[ $ENV == 'test' || $ENV == 'live' ]]; then
   if [[ -z $1 ]]; then
     while [[ -z $DEPLOY_MSG ]]; do
-      echo;
-      read -p "Enter deployment message: " DEPLOY_MSG;
-      echo;
+      echo
+      read -p "Enter deployment message: " DEPLOY_MSG
+      echo
     done
   else
-    DEPLOY_MSG=$@;
+    DEPLOY_MSG=$@
   fi
 fi
 
-NOW=`date +%Y%m%d_%H%M`;
-LOG_DIR="deploy-$ENV-$NOW";
-mkdir $LOG_DIR;
-echo "LOG_DIR: $LOG_DIR";
+NOW=$(date +%Y%m%d_%H%M)
+LOG_DIR="deploy-$ENV-$NOW"
+mkdir $LOG_DIR
+echo "LOG_DIR: $LOG_DIR"
 
-terminus org:site:list ohio-state-arts-and-sciences --upstream=5161e51a-0e4a-414c-974e-8565094b76b1 --tag=D8 --fields=name --format=string | sort | tee $LOG_DIR/site_list.txt;
+terminus org:site:list ohio-state-arts-and-sciences --upstream=5161e51a-0e4a-414c-974e-8565094b76b1 --tag=D8 --fields=name --format=string | sort | tee $LOG_DIR/site_list.txt
 
-parallel --delay 0.2 --tmuxpane --fg -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log";
+parallel --delay 0.2 -j 36 -a $LOG_DIR/site_list.txt "scripts/deploy-site-env.sh {}.$ENV $DEPLOY_MSG 2>&1 | tee $LOG_DIR/{}.log"
 
-echo; echo; echo "Sleeping for 11 seconds..."; echo; echo;
-sleep 11;
+echo
+echo
+echo "Sleeping for 11 seconds..."
+echo
+echo
+sleep 11
 
-reset;
+reset
 
 # wait;
 
-cd $LOG_DIR;
-wc -l * | sort -n | tee line-counts.txt;
+cd $LOG_DIR
+wc -l * | sort -n | tee line-counts.txt
 
-MERGE_ERR=$(grep -rnw './' -e "Merge conflict");
-if [[ -n ${MERGE_ERR} ]];
-then
-    echo; echo "It looks there were some errors. Review _merge-conflicts.txt for more information."; echo;
-    echo "$MERGE_ERR" > ./_merge-conflicts.txt;
+MERGE_ERR=$(grep -rnw './' -e "Merge conflict")
+if [[ -n ${MERGE_ERR} ]]; then
+  echo
+  echo "It looks there were some errors. Review _merge-conflicts.txt for more information."
+  echo
+  echo "$MERGE_ERR" >./_merge-conflicts.txt
 else
-    echo; echo "No automatic merge issues detected. Success?"; echo;
+  echo
+  echo "No automatic merge issues detected. Success?"
+  echo
 fi
-cd ..;
+cd ..
 
-echo; echo; echo "Sleeping for 11 more seconds..."; echo; echo;
-sleep 11;
+echo
+echo
+echo "Sleeping for 11 more seconds..."
+echo
+echo
+sleep 11
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index abef8a072ab979df77d2ef22d43eaa3a1b3ebb82..20275aebac29b0da29ea3c83b81efa4ea271f584 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -5291,37 +5291,38 @@
         },
         {
             "name": "drupal/linkit",
-            "version": "5.0.0-beta13",
-            "version_normalized": "5.0.0.0-beta13",
+            "version": "6.0.0",
+            "version_normalized": "6.0.0.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/linkit.git",
-                "reference": "8.x-5.0-beta13"
+                "reference": "6.0.0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/linkit-8.x-5.0-beta13.zip",
-                "reference": "8.x-5.0-beta13",
-                "shasum": "9215fbea84166cabc9b7a2d9a04dedaffb9fc1ed"
+                "url": "https://ftp.drupal.org/files/projects/linkit-6.0.0.zip",
+                "reference": "6.0.0",
+                "shasum": "3c4143eb797abee04e5af47eb1885a65e6321b51"
             },
             "require": {
-                "drupal/core": "^8.7.7 || ^9"
+                "drupal/core": "^9.4 || ^10.0.0"
+            },
+            "conflict": {
+                "drupal/core": ">=10.1"
             },
             "require-dev": {
+                "drupal/ckeditor": "*",
                 "drupal/imce": "*"
             },
             "type": "drupal-module",
             "extra": {
                 "drupal": {
-                    "version": "8.x-5.0-beta13",
-                    "datestamp": "1632933670",
+                    "version": "6.0.0",
+                    "datestamp": "1688748025",
                     "security-coverage": {
-                        "status": "not-covered",
-                        "message": "Beta releases are not covered by Drupal security advisories."
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
                     }
-                },
-                "patches_applied": {
-                    "2712951": "https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch"
                 }
             },
             "installation-source": "dist",
@@ -5339,6 +5340,10 @@
                 {
                     "name": "johnwebdev",
                     "homepage": "https://www.drupal.org/user/3331569"
+                },
+                {
+                    "name": "mark_fullmer",
+                    "homepage": "https://www.drupal.org/user/2612816"
                 }
             ],
             "description": "Linkit - Enriched linking experience",
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index d8bae5bfa0ec8eac6bb42401e74118c2c297631a..e69ca4f69b11d927c8825258b596d2a63c5da29f 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'osu-asc-webservices/d8-upstream',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '4cbb2152350103f297f5852d6a6816b907061d82',
+        'reference' => 'eedf8cf2fe6599bb32dea9e590be4b8a93b63825',
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -887,9 +887,9 @@
             'dev_requirement' => false,
         ),
         'drupal/linkit' => array(
-            'pretty_version' => '5.0.0-beta13',
-            'version' => '5.0.0.0-beta13',
-            'reference' => '8.x-5.0-beta13',
+            'pretty_version' => '6.0.0',
+            'version' => '6.0.0.0',
+            'reference' => '6.0.0',
             'type' => 'drupal-module',
             'install_path' => __DIR__ . '/../../web/modules/linkit',
             'aliases' => array(),
@@ -1549,7 +1549,7 @@
         'osu-asc-webservices/d8-upstream' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '4cbb2152350103f297f5852d6a6816b907061d82',
+            'reference' => 'eedf8cf2fe6599bb32dea9e590be4b8a93b63825',
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
diff --git a/web/modules/linkit/.tugboat/config.yml b/web/modules/linkit/.tugboat/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9e8c4bb26ff05d9aad3a02271af1bac49a7c6836
--- /dev/null
+++ b/web/modules/linkit/.tugboat/config.yml
@@ -0,0 +1,52 @@
+# @see: https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal/using-live-previews-on-drupal-core-and-contrib-merge-requests#s-adding-live-previews-to-a-contributed-module
+services:
+  php:
+    image: q0rban/tugboat-drupal:10
+    default: true
+    http: false
+    depends: mysql
+    commands:
+      update: |
+        set -eux
+        # Check out a branch using the unique Tugboat ID for this repository, to
+        # ensure we don't clobber an existing branch.
+        git checkout -b $TUGBOAT_REPO_ID
+        # Composer is hungry. You need a Tugboat project with a pretty sizeable
+        # chunk of memory.
+        export COMPOSER_MEMORY_LIMIT=-1
+        # This is an environment variable we added in the Dockerfile that
+        # provides the path to Drupal composer root (not the web root).
+        cd $DRUPAL_COMPOSER_ROOT
+        # We configure the Drupal project to use the checkout of the module as a
+        # Composer package repository.
+        composer config repositories.tugboat vcs $TUGBOAT_ROOT
+        # Now we can require this module, specifing the branch name we created
+        # above that uses the $TUGBOAT_REPO_ID environment variable.
+        composer require drupal/linkit:dev-$TUGBOAT_REPO_ID
+        # Install Drupal on the site.
+        vendor/bin/drush \
+          --yes \
+          --db-url=mysql://tugboat:tugboat@mysql:3306/tugboat \
+          --site-name="Live preview for ${TUGBOAT_PREVIEW_NAME}" \
+          --account-pass=admin \
+          site:install standard
+        # Set up the files directory permissions.
+        mkdir -p $DRUPAL_DOCROOT/sites/default/files
+        chgrp -R www-data $DRUPAL_DOCROOT/sites/default/files
+        chmod 2775 $DRUPAL_DOCROOT/sites/default/files
+        chmod -R g+w $DRUPAL_DOCROOT/sites/default/files
+        # Enable the module.
+        vendor/bin/drush --yes pm:enable linkit
+      build: |
+        set -eux
+        # Delete and re-check out this branch in case this is built from a Base Preview.
+        git branch -D $TUGBOAT_REPO_ID && git checkout -b $TUGBOAT_REPO_ID || true
+        export COMPOSER_MEMORY_LIMIT=-1
+        cd $DRUPAL_COMPOSER_ROOT
+        composer install --optimize-autoloader
+        # Update this module, including all dependencies.
+        composer update drupal/linkit --with-all-dependencies
+        vendor/bin/drush --yes updb
+        vendor/bin/drush cache:rebuild
+  mysql:
+    image: tugboatqa/mariadb
diff --git a/web/modules/linkit/PATCHES.txt b/web/modules/linkit/PATCHES.txt
deleted file mode 100644
index ad45756bcc49243ecb529af58a6e4b22ca371ea8..0000000000000000000000000000000000000000
--- a/web/modules/linkit/PATCHES.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)
-Patches applied to this directory:
-
-2712951
-Source: https://www.drupal.org/files/issues/2021-04-07/linkit-for-link-field-2712951-216.patch
-
-
diff --git a/web/modules/linkit/README.md b/web/modules/linkit/README.md
index b4ecac50c56cccc2c6b58195b8a1aa7df7c2c392..fb547c87a499a09a3d49da2cc49d66b65dd616b6 100644
--- a/web/modules/linkit/README.md
+++ b/web/modules/linkit/README.md
@@ -1,9 +1,9 @@
 INTRODUCTION
 ------------
-Linkit provides an **enriched linking experience for internal and 
-external linking** with editors by using an autocomplete field. Linkit 
-has by default support for nodes, users, taxonomy terms, files, 
-comments and **basic support for all types of entities** that defines a 
+Linkit provides an **enriched linking experience for internal and
+external linking** with editors by using an autocomplete field. Linkit
+has by default support for nodes, users, taxonomy terms, files,
+comments and **basic support for all types of entities** that defines a
 canonical link template.
 
  * For a full description of the module, visit the project page:
@@ -12,6 +12,23 @@ canonical link template.
  * To submit bug reports and feature suggestions, or track changes:
    https://www.drupal.org/project/issues/linkit
 
+
+DRUPAL CORE FEATURE COMPARISON
+------------------------------
+Drupal core will provide link autocomplete suggestions in CKEditor. See
+https://www.drupal.org/project/drupal/issues/3317769
+
+Below is a list of features included in Linkit which will not initially be in
+Drupal core.
+
+ * Configurable autocomplete on link fields.
+ * Ability to configure metadata (with token support) for autcomplete suggestions
+ * Ability to control number of items shown
+ * Ability to toggle published/unpublished entities in suggestions
+ * IMCE integration
+ * Pluggable, configurable, and alterable matchers
+ * Pluggable, configurable, and alterable attributes
+
 REQUIREMENTS
 ------------
 
@@ -25,16 +42,16 @@ INSTALLATION
 https://www.drupal.org/documentation/install/modules-themes/modules-8
 
 * **Enable Linkit**
-To enable Linkit, go to `/admin/config/content/formats` and edit the 
-desired text format you want to enable Linkit for. Linkit will alter 
-the default link plugin, so make sure that it is enabled. When the 
-default link plugin is enabled, you will have to select a Linkit 
+To enable Linkit, go to `/admin/config/content/formats` and edit the
+desired text format you want to enable Linkit for. Linkit will alter
+the default link plugin, so make sure that it is enabled. When the
+default link plugin is enabled, you will have to select a Linkit
 profile to use in the "Drupal link" tab under the toolbar configuration.
 
 * **Enable Linkit filter**
-Linkit will insert URLs in a format like "entity:node/1". The Linkit 
-filter will then transform that URL into a "real" URL when rendering 
-the text. **Note: The Linkit filter must run before "Limit allowed HTML 
+Linkit will insert URLs in a format like "entity:node/1". The Linkit
+filter will then transform that URL into a "real" URL when rendering
+the text. **Note: The Linkit filter must run before "Limit allowed HTML
 tags and correct faulty HTML"**.
 
 * If the **Limit allowed HTML tags and correct faulty HTML** filter is
@@ -51,8 +68,8 @@ If automatic titles is enabled in the Linkit filter settings, and
 CONFIGURATION
 -------------
 
-A default Linkit profile will have been installed as a step in the 
-module installation process. The profile will contain information about 
+A default Linkit profile will have been installed as a step in the
+module installation process. The profile will contain information about
 which plugins to use.
 
 You can create additional profiles at `/admin/config/content/linkit`.
@@ -61,7 +78,7 @@ You can create additional profiles at `/admin/config/content/linkit`.
 PLUGIN EXAMPLES
 ---------------
 
-There are plugin implementation examples in the linkit_test module 
+There are plugin implementation examples in the linkit_test module
 bundled with Linkit core.
 
 
@@ -70,3 +87,4 @@ MAINTAINERS
 
 Current maintainers:
  * Emil Stjerneman (anon) - https://www.drupal.org/user/464598
+ * Mark Fullmer (mark_fullmer) - https://www.drupal.org/u/mark_fullmer
diff --git a/web/modules/linkit/composer.json b/web/modules/linkit/composer.json
index eb28ea2f63458db99b4096d3f0dde4945fb554dd..0c178d7949d88290806881883ed09f1c86f58604 100644
--- a/web/modules/linkit/composer.json
+++ b/web/modules/linkit/composer.json
@@ -16,7 +16,10 @@
     "source": "http://cgit.drupalcode.org/linkit"
   },
   "require" : {
-    "drupal/core": "^8.7.7 || ^9"
+    "drupal/core": "^9.4 || ^10.0.0"
+  },
+  "conflict": {
+    "drupal/core": ">=10.1"
   },
   "license": "GPL-2.0-or-later"
 }
diff --git a/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml b/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml
index 69792fe03b19012bf69eaea471c29f786baf353f..1d6d88ea43991f398409d7139cf8c9728a7bc635 100644
--- a/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml
+++ b/web/modules/linkit/config/optional/image.style.linkit_result_thumbnail.yml
@@ -11,3 +11,4 @@ effects:
     data:
       width: 50
       height: 50
+      anchor: center-center
diff --git a/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml b/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml
index 7aa4092fa8399d94412db01caadba9365a19faee..6708c63490ce440f8dd6c6edc5d3e7df0cf69318 100644
--- a/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml
+++ b/web/modules/linkit/config/optional/linkit.linkit_profile.default.yml
@@ -12,8 +12,9 @@ matchers:
     id: 'entity:node'
     weight: 0
     settings:
-      metadata: 'by [node:author] | [node:created:medium]'
+      metadata: '[node:content-type:name] #[node:nid] | [node:created:medium] by [node:author]'
       bundles: {  }
       group_by_bundle: false
       include_unpublished: false
       substitution_type: canonical
+      limit: 100
diff --git a/web/modules/linkit/config/schema/linkit.schema.yml b/web/modules/linkit/config/schema/linkit.schema.yml
index 3787e95c9abca4a8bcac582ee501287bca102c0f..f9eef21c4b3dd957c9b5c19be1f0500e61862a2d 100644
--- a/web/modules/linkit/config/schema/linkit.schema.yml
+++ b/web/modules/linkit/config/schema/linkit.schema.yml
@@ -133,3 +133,19 @@ field.formatter.settings.linkit:
     linkit_profile:
       type: string
       label: 'Linkit profile'
+
+ckeditor5.plugin.linkit_extension:
+  type: mapping
+  label: Linkit
+  constraints:
+    Callback: [\Drupal\linkit\Plugin\CKEditor5Plugin\Linkit, requireProfileIfEnabled]
+  mapping:
+    linkit_enabled:
+      type: boolean
+      label: 'Use Linkit'
+    linkit_profile:
+      type: string
+      label: 'Linkit profile'
+      constraints:
+        Choice:
+          callback: \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::validChoices
diff --git a/web/modules/linkit/css/linkit.autocomplete.css b/web/modules/linkit/css/linkit.autocomplete.css
index 4159a495f94a7967384c64bee1f9784c370c4aec..2476f91d3a14b4f1f474a95ee403cf2f433991e2 100644
--- a/web/modules/linkit/css/linkit.autocomplete.css
+++ b/web/modules/linkit/css/linkit.autocomplete.css
@@ -25,10 +25,13 @@
 .linkit-ui-autocomplete {
   max-height: calc((100vh - 80px)/2);
   overflow: auto;
+  position: relative;
 }
 
 .linkit-ui-autocomplete.ui-widget {
   font-size: .9em;
+  max-width: inherit;
+  position: absolute;
 }
 
 .linkit-ui-autocomplete.ui-menu .linkit-result-line-wrapper {
@@ -44,6 +47,11 @@
   color: #fff;
 }
 
+.ui-autocomplete .linkit-result-line-wrapper.ui-menu-item-wrapper.ui-state-active,
+.ui-autocomplete .linkit-result-line-wrapper.ui-menu-item-wrapper.ui-state-focus {
+  background: #bfbfbf;
+}
+
 .linkit-result-line:not(:last-of-type) {
   border-bottom: 1px solid #bfbfbf;
 }
diff --git a/web/modules/linkit/js/build/linkit.js b/web/modules/linkit/js/build/linkit.js
new file mode 100644
index 0000000000000000000000000000000000000000..d45e048b744171a0763519f333c17f6c64eda08e
--- /dev/null
+++ b/web/modules/linkit/js/build/linkit.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.CKEditor5=e():(t.CKEditor5=t.CKEditor5||{},t.CKEditor5.linkit=e())}(self,(()=>(()=>{var t={"ckeditor5/src/core.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/typing.js":(t,e,i)=>{t.exports=i("dll-reference CKEditor5.dll")("./src/typing.js")},"dll-reference CKEditor5.dll":t=>{"use strict";t.exports=CKEditor5.dll}},e={};function i(n){var o=e[n];if(void 0!==o)return o.exports;var s=e[n]={exports:{}};return t[n](s,s.exports,i),s.exports}i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var n={};return(()=>{"use strict";i.d(n,{default:()=>u});var t=i("ckeditor5/src/core.js"),e=i("ckeditor5/src/typing.js");class o extends t.Plugin{init(){this.attrs=["data-entity-type","data-entity-uuid","data-entity-substitution"],this._allowAndConvertExtraAttributes(),this._removeExtraAttributesOnUnlinkCommandExecute(),this._refreshExtraAttributeValues(),this._addExtraAttributesOnLinkCommandExecute()}_allowAndConvertExtraAttributes(){const t=this.editor;t.model.schema.extend("$text",{allowAttributes:this.attrs}),this.attrs.forEach((e=>{t.conversion.for("downcast").attributeToElement({model:e,view:(t,{writer:i})=>{const n=i.createAttributeElement("a",{[e]:t},{priority:5});return i.setCustomProperty("link",!0,n),n}}),t.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{[e]:!0}},model:{key:e,value:t=>t.getAttribute(e)}})}))}_addExtraAttributesOnLinkCommandExecute(){const t=this.editor,e=t.commands.get("link");let i=!1;e.on("execute",((e,n)=>{if(n.length<3)return;if(i)return void(i=!1);e.stop(),i=!0;const o=n[n.length-1],s=this.editor.model,r=s.document.selection;s.change((e=>{t.execute("link",...n);const i=r.getFirstPosition();this.attrs.forEach((t=>{if(r.isCollapsed){const n=i.textNode||i.nodeBefore;o[t]?e.setAttribute(t,o[t],e.createRangeOn(n)):e.removeAttribute(t,e.createRangeOn(n)),e.removeSelectionAttribute(t)}else{const i=s.schema.getValidRanges(r.getRanges(),t);for(const n of i)o[t]?e.setAttribute(t,o[t],n):e.removeAttribute(t,n)}}))}))}),{priority:"high"})}_removeExtraAttributesOnUnlinkCommandExecute(){const t=this.editor,i=t.commands.get("unlink"),n=this.editor.model,o=n.document.selection;let s=!1;i.on("execute",(i=>{s||(i.stop(),n.change((()=>{s=!0,t.execute("unlink"),s=!1,n.change((t=>{let i;this.attrs.forEach((s=>{i=o.isCollapsed?[(0,e.findAttributeRange)(o.getFirstPosition(),s,o.getAttribute(s),n)]:n.schema.getValidRanges(o.getRanges(),s);for(const e of i)t.removeAttribute(s,e)}))}))})))}),{priority:"high"})}_refreshExtraAttributeValues(){const t=this.editor,e=this.attrs,i=t.commands.get("link"),n=this.editor.model,o=n.document.selection;e.forEach((t=>{i.set(t,null)})),n.document.on("change",(()=>{e.forEach((t=>{i[t]=o.getAttribute(t)}))}))}static get pluginName(){return"LinkitEditing"}}const s=jQuery;function r(t,e){var i=s("<li>").addClass("linkit-result-line"),n=s("<div>").addClass("linkit-result-line-wrapper");return n.append(s("<span>").html(e.label).addClass("linkit-result-line--title")),e.hasOwnProperty("description")&&n.append(s("<span>").html(e.description).addClass("linkit-result-line--description")),i.append(n).appendTo(t)}function a(t,e){var i=this.element.autocomplete("instance"),n={};e.forEach((function(t){const e=t.hasOwnProperty("group")?t.group:"";n.hasOwnProperty(e)||(n[e]=[]),n[e].push(t)})),s.each(n,(function(e,n){e.length&&t.append('<li class="linkit-result-line--group ui-menu-divider">'+e+"</li>"),s.each(n,(function(e,n){i._renderItemData(t,n)}))}))}class l extends t.Plugin{static get requires(){return[o]}init(){this._state={};this.editor.config.get("linkit");this._enableLinkAutocomplete(),this._handleExtraFormFieldSubmit(),this._handleDataLoadingIntoExtraFormField()}_enableLinkAutocomplete(){const t=this.editor,e=t.config.get("linkit"),i=t.plugins.get("LinkUI").formView;let n=!1;i.extendTemplate({attributes:{class:["ck-vertical-form","ck-link-form_layout-vertical"]}}),t.plugins.get("ContextualBalloon")._rotatorView.content.on("add",((t,o)=>{if(o!==i||n)return;let l;!function(t,e){const{autocompleteUrl:i,selectHandler:n,closeHandler:o,openHandler:l}=e,u={cache:{},ajax:{dataType:"json",jsonp:!1}},d={appendTo:t.closest(".ck-labeled-field-view"),source:function(t,e){const{cache:n}=u;var o=t.term;n.hasOwnProperty(o)?e(n[o]):s.ajax(i,{success:function(t){n[o]=t.suggestions,e(t.suggestions)},data:{q:o},...u.ajax})},select:n,focus:()=>!1,search:()=>!d.isComposing,close:o,open:l,minLength:1,isComposing:!1},c=s(t).autocomplete(d),p=c.data("ui-autocomplete");p.widget().menu("option","items","> :not(.linkit-result-line--group)"),p._renderMenu=a,p._renderItem=r,c.autocomplete("widget").addClass("linkit-ui-autocomplete ck-reset_all-excluded"),c.on("click",(function(){c.autocomplete("search",c.val())})),c.on("compositionstart.autocomplete",(function(){d.isComposing=!0})),c.on("compositionend.autocomplete",(function(){d.isComposing=!1}))}(i.urlInputView.fieldView.element,{...e,selectHandler:(t,{item:e})=>{if(!e.path)throw"Missing path param."+JSON.stringify(e);if(e.entity_type_id||e.entity_uuid||e.substitution_id){if(!e.entity_type_id||!e.entity_uuid||!e.substitution_id)throw"Missing path param."+JSON.stringify(e);this.set("entityType",e.entity_type_id),this.set("entityUuid",e.entity_uuid),this.set("entitySubstitution",e.substitution_id)}else this.set("entityType",null),this.set("entityUuid",null),this.set("entitySubstitution",null);return t.target.value=e.path,l=!0,!1},openHandler:t=>{l=!1},closeHandler:t=>{l||(this.set("entityType",null),this.set("entityUuid",null),this.set("entitySubstitution",null)),l=!1}}),n=!0,i.urlInputView.fieldView.template.attributes.class.push("form-linkit-autocomplete")}))}_handleExtraFormFieldSubmit(){const t=this.editor,e=t.plugins.get("LinkUI").formView,i=t.commands.get("link");this.listenTo(e,"submit",(()=>{const t={"data-entity-type":this.entityType,"data-entity-uuid":this.entityUuid,"data-entity-substitution":this.entitySubstitution};i.once("execute",((e,i)=>{if(i.length<3)i.push(t);else{if(3!==i.length)throw Error("The link command has more than 3 arguments.");Object.assign(i[2],t)}}),{priority:"highest"})}),{priority:"high"})}_handleDataLoadingIntoExtraFormField(){const t=this.editor.commands.get("link");this.bind("entityType").to(t,"data-entity-type"),this.bind("entityUuid").to(t,"data-entity-uuid"),this.bind("entitySubstitution").to(t,"data-entity-substitution")}static get pluginName(){return"Linkit"}}const u={Linkit:l}})(),n=n.default})()));
\ No newline at end of file
diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md b/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e6d004a540bb7450b34d433cdc3efb226c5e573d
--- /dev/null
+++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/README.md
@@ -0,0 +1,9 @@
+This plugin is largely based on CKEditor 5's [block plugin widget tutorial](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html),
+but with added documentation to facilitate better understanding of CKEditor 5
+plugin development and other minor changes.
+
+Within `/src` are the multiple files that will be used by the build process to
+become a CKEditor 5 plugin in `/build`. Technically, everything in these files
+could be in a single `index.js` - the only file the MUST be present is
+`/src/index.js`. However, splitting the plugin into concern-specific files has
+maintainability benefits.
diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js
new file mode 100644
index 0000000000000000000000000000000000000000..be9d908bebabc2e0e712fda941cb6e2f4697056f
--- /dev/null
+++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/autocomplete.js
@@ -0,0 +1,137 @@
+const $ = jQuery;
+
+
+/**
+ * Override jQuery UI _renderItem function to output HTML by default.
+ *
+ * @param {object} ul
+ *   The <ul> element that the newly created <li> element must be appended to.
+ * @param {object} item
+ *  The list item to append.
+ *
+ * @return {object}
+ *   jQuery collection of the ul element.
+ */
+function renderItem(ul, item) {
+  var $line = $('<li>').addClass('linkit-result-line');
+  var $wrapper = $('<div>').addClass('linkit-result-line-wrapper');
+  $wrapper.append($('<span>').html(item.label).addClass('linkit-result-line--title'));
+
+  if (item.hasOwnProperty('description')) {
+    $wrapper.append($('<span>').html(item.description).addClass('linkit-result-line--description'));
+  }
+  return $line.append($wrapper).appendTo(ul);
+}
+
+/**
+ * Override jQuery UI _renderMenu function to handle groups.
+ *
+ * @param {object} ul
+ *   An empty <ul> element to use as the widget's menu.
+ * @param {array} items
+ *   An Array of items that match the user typed term.
+ */
+function renderMenu(ul, items) {
+  var self = this.element.autocomplete('instance');
+
+  var grouped_items = {};
+  items.forEach(function (item) {
+    const group = item.hasOwnProperty('group') ? item.group : '';
+    if (!grouped_items.hasOwnProperty(group)) {
+      grouped_items[group] = [];
+    }
+    grouped_items[group].push(item);
+  });
+
+  $.each(grouped_items, function (group, items) {
+    if (group.length) {
+      ul.append('<li class="linkit-result-line--group ui-menu-divider">' + group + '</li>');
+    }
+
+    $.each(items, function (index, item) {
+      self._renderItemData(ul, item);
+    });
+  });
+}
+
+export default function initializeAutocomplete(element, settings) {
+  const { autocompleteUrl, selectHandler, closeHandler, openHandler } = settings;
+  const autocomplete = {
+    cache: {},
+    ajax: {
+      dataType: 'json',
+      jsonp: false,
+    },
+  };
+
+  /**
+   * JQuery UI autocomplete source callback.
+   *
+   * @param {object} request
+   *   The request object.
+   * @param {function} response
+   *   The function to call with the response.
+   */
+  function sourceData(request, response) {
+    const { cache } = autocomplete;
+    /**
+     * Transforms the data object into an array and update autocomplete results.
+     *
+     * @param {object} data
+     *   The data sent back from the server.
+     */
+    function sourceCallbackHandler(data) {
+      cache[term] = data.suggestions;
+      response(data.suggestions);
+    }
+
+    // Get the desired term and construct the autocomplete URL for it.
+    var term = request.term;
+
+    // Check if the term is already cached.
+    if (cache.hasOwnProperty(term)) {
+      response(cache[term]);
+    }
+    else {
+      $.ajax(autocompleteUrl, {
+        success: sourceCallbackHandler,
+        data: {q: term},
+        ...autocomplete.ajax,
+      });
+    }
+  }
+
+  const options = {
+    appendTo: element.closest('.ck-labeled-field-view'),
+    source: sourceData,
+    select: selectHandler,
+    focus: () => false,
+    search: () => !options.isComposing,
+    close: closeHandler,
+    open: openHandler,
+    minLength: 1,
+    isComposing: false,
+  }
+  const $auto = $(element).autocomplete(options);
+
+  // Override a few things.
+  const instance = $auto.data('ui-autocomplete');
+  instance.widget().menu('option', 'items', '> :not(.linkit-result-line--group)');
+  instance._renderMenu = renderMenu;
+  instance._renderItem = renderItem;
+
+  $auto.autocomplete('widget').addClass('linkit-ui-autocomplete ck-reset_all-excluded');
+
+  $auto.on('click', function () {
+    $auto.autocomplete('search', $auto.val());
+  });
+
+  $auto.on('compositionstart.autocomplete', function () {
+    options.isComposing = true;
+  });
+  $auto.on('compositionend.autocomplete', function () {
+    options.isComposing = false;
+  });
+
+  return $auto;
+}
diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a33ccbeed7729c9c392f609f72064026e8e804b
--- /dev/null
+++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/index.js
@@ -0,0 +1,143 @@
+import { Plugin } from 'ckeditor5/src/core';
+import LinkitEditing from './linkitediting';
+import initializeAutocomplete from './autocomplete';
+
+class Linkit extends Plugin {
+  /**
+   * @inheritdoc
+   */
+  static get requires() {
+    return [LinkitEditing];
+  }
+
+  init() {
+    this._state = {};
+    const editor = this.editor;
+    const options = editor.config.get('linkit');
+    this._enableLinkAutocomplete();
+    this._handleExtraFormFieldSubmit();
+    this._handleDataLoadingIntoExtraFormField();
+  }
+
+  _enableLinkAutocomplete() {
+    const editor = this.editor;
+    const options = editor.config.get('linkit');
+    const linkFormView = editor.plugins.get( 'LinkUI' ).formView;
+    let wasAutocompleteAdded = false;
+
+    linkFormView.extendTemplate( {
+      attributes: {
+        class: ['ck-vertical-form', 'ck-link-form_layout-vertical']
+      }
+    } );
+
+    editor.plugins.get( 'ContextualBalloon' )._rotatorView.content.on('add', ( evt, view ) => {
+      if ( view !== linkFormView || wasAutocompleteAdded ) {
+        return;
+      }
+
+      /**
+       * Used to know if a selection was made from the autocomplete results.
+       *
+       * @type {boolean}
+       */
+      let selected;
+
+      initializeAutocomplete(
+        linkFormView.urlInputView.fieldView.element,
+        {
+          ...options,
+          selectHandler: (event, { item }) => {
+            if (!item.path) {
+              throw 'Missing path param.' + JSON.stringify(item);
+            }
+
+            if (item.entity_type_id || item.entity_uuid || item.substitution_id) {
+              if (!item.entity_type_id || !item.entity_uuid || !item.substitution_id) {
+                throw 'Missing path param.' + JSON.stringify(item);
+              }
+
+              this.set('entityType', item.entity_type_id);
+              this.set('entityUuid', item.entity_uuid);
+              this.set('entitySubstitution', item.substitution_id);
+            }
+            else {
+              this.set('entityType', null);
+              this.set('entityUuid', null);
+              this.set('entitySubstitution', null);
+            }
+
+            event.target.value = item.path;
+            selected = true;
+            return false;
+          },
+          openHandler: (event) => {
+            selected = false;
+          },
+          closeHandler: (event) => {
+            if (!selected) {
+              this.set('entityType', null);
+              this.set('entityUuid', null);
+              this.set('entitySubstitution', null);
+            }
+            selected = false;
+          },
+        },
+      );
+
+      wasAutocompleteAdded = true;
+      linkFormView.urlInputView.fieldView.template.attributes.class.push('form-linkit-autocomplete');
+    });
+  }
+
+  _handleExtraFormFieldSubmit() {
+    const editor = this.editor;
+    const linkFormView = editor.plugins.get('LinkUI').formView;
+    const linkCommand = editor.commands.get('link');
+
+    this.listenTo(linkFormView, 'submit', () => {
+      const values = {
+        'data-entity-type': this.entityType,
+        'data-entity-uuid': this.entityUuid,
+        'data-entity-substitution': this.entitySubstitution,
+      }
+      // Stop the execution of the link command caused by closing the form.
+      // Inject the extra attribute value. The highest priority listener here
+      // injects the argument (here below 👇).
+      // - The high priority listener in
+      //   _addExtraAttributeOnLinkCommandExecute() gets that argument and sets
+      //   the extra attribute.
+      // - The normal (default) priority listener in ckeditor5-link sets
+      //   (creates) the actual link.
+      linkCommand.once('execute', (evt, args) => {
+        if (args.length < 3) {
+          args.push(values);
+        } else if (args.length === 3) {
+          Object.assign(args[2], values);
+        } else {
+          throw Error('The link command has more than 3 arguments.')
+        }
+      }, { priority: 'highest' });
+    }, { priority: 'high' });
+  }
+
+  _handleDataLoadingIntoExtraFormField() {
+    const editor = this.editor;
+    const linkCommand = editor.commands.get('link');
+
+    this.bind('entityType').to(linkCommand, 'data-entity-type');
+    this.bind('entityUuid').to(linkCommand, 'data-entity-uuid');
+    this.bind('entitySubstitution').to(linkCommand, 'data-entity-substitution');
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'Linkit';
+  }
+}
+
+export default {
+  Linkit,
+};
diff --git a/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e1737621d8c6015649312c9e4a548feea865e7d
--- /dev/null
+++ b/web/modules/linkit/js/ckeditor5_plugins/linkit/src/linkitediting.js
@@ -0,0 +1,191 @@
+import { Plugin } from 'ckeditor5/src/core';
+import { findAttributeRange } from 'ckeditor5/src/typing';
+
+export default class LinkitEditing extends Plugin {
+  init() {
+    this.attrs = ['data-entity-type', 'data-entity-uuid', 'data-entity-substitution'];
+    this._allowAndConvertExtraAttributes();
+    this._removeExtraAttributesOnUnlinkCommandExecute();
+    this._refreshExtraAttributeValues();
+    this._addExtraAttributesOnLinkCommandExecute();
+  }
+
+  _allowAndConvertExtraAttributes() {
+    const editor = this.editor;
+
+    editor.model.schema.extend('$text', { allowAttributes: this.attrs });
+
+    // Model -> View (DOM)
+    this.attrs.forEach((attribute) => {
+      editor.conversion.for('downcast').attributeToElement({
+        model: attribute,
+        view: (value, { writer }) => {
+          const linkViewElement = writer.createAttributeElement('a', {
+            [attribute]: value
+          }, { priority: 5 });
+
+          // Without it the isLinkElement() will not recognize the link and the UI will not show up
+          // when the user clicks a link.
+          writer.setCustomProperty('link', true, linkViewElement);
+
+          return linkViewElement;
+        }
+      });
+
+      // View (DOM/DATA) -> Model
+      editor.conversion.for('upcast')
+        .elementToAttribute({
+          view: {
+            name: 'a',
+            attributes: {
+              [attribute]: true,
+            }
+          },
+          model: {
+            key: attribute,
+            value: viewElement => viewElement.getAttribute(attribute),
+          }
+        });
+    });
+  }
+
+  _addExtraAttributesOnLinkCommandExecute() {
+    const editor = this.editor;
+    const linkCommand = editor.commands.get('link');
+    let linkCommandExecuting = false;
+
+    linkCommand.on('execute', (evt, args) => {
+      // Custom handling is only required if an extra attribute was passed into
+      // editor.execute( 'link', ... ).
+      if (args.length < 3) {
+        return;
+      }
+      if (linkCommandExecuting) {
+        linkCommandExecuting = false;
+        return;
+      }
+
+      // If the additional attribute was passed, we stop the default execution
+      // of the LinkCommand. We're going to create Model#change() block for undo
+      // and execute the LinkCommand together with setting the extra attribute.
+      evt.stop();
+
+      // Prevent infinite recursion by keeping records of when link command is
+      // being executed by this function.
+      linkCommandExecuting = true;
+      const extraAttributeValues = args[args.length - 1];
+      const model = this.editor.model;
+      const selection = model.document.selection;
+
+      // Wrapping the original command execution in a model.change() block to
+      // make sure there's a single undo step when the extra attribute is added.
+      model.change((writer) => {
+        editor.execute('link', ...args);
+
+        const firstPosition = selection.getFirstPosition();
+
+        this.attrs.forEach((attribute) => {
+          if (selection.isCollapsed) {
+            const node = firstPosition.textNode || firstPosition.nodeBefore;
+
+            if (extraAttributeValues[attribute]) {
+              writer.setAttribute(attribute, extraAttributeValues[attribute], writer.createRangeOn(node));
+            } else {
+              writer.removeAttribute(attribute, writer.createRangeOn(node));
+            }
+
+            writer.removeSelectionAttribute(attribute);
+          } else {
+            const ranges = model.schema.getValidRanges(selection.getRanges(), attribute);
+
+            for (const range of ranges) {
+              if (extraAttributeValues[attribute]) {
+                writer.setAttribute(attribute, extraAttributeValues[attribute], range);
+              } else {
+                writer.removeAttribute(attribute, range);
+              }
+            }
+          }
+        });
+      });
+    }, { priority: 'high' } );
+  }
+
+  _removeExtraAttributesOnUnlinkCommandExecute() {
+    const editor = this.editor;
+    const unlinkCommand = editor.commands.get('unlink');
+    const model = this.editor.model;
+    const selection = model.document.selection;
+
+    let isUnlinkingInProgress = false;
+
+    // Make sure all changes are in a single undo step so cancel the original unlink first in the high priority.
+    unlinkCommand.on('execute', evt => {
+      if (isUnlinkingInProgress) {
+        return;
+      }
+
+      evt.stop();
+
+      // This single block wraps all changes that should be in a single undo step.
+      model.change(() => {
+        // Now, in this single "undo block" let the unlink command flow naturally.
+        isUnlinkingInProgress = true;
+
+        // Do the unlinking within a single undo step.
+        editor.execute('unlink');
+
+        // Let's make sure the next unlinking will also be handled.
+        isUnlinkingInProgress = false;
+
+        // The actual integration that removes the extra attribute.
+        model.change(writer => {
+          // Get ranges to unlink.
+          let ranges;
+
+          this.attrs.forEach((attribute) => {
+            if (selection.isCollapsed) {
+              ranges = [findAttributeRange(
+                selection.getFirstPosition(),
+                attribute,
+                selection.getAttribute(attribute),
+                model
+              )];
+            } else {
+              ranges = model.schema.getValidRanges(selection.getRanges(), attribute);
+            }
+
+            // Remove the extra attribute from specified ranges.
+            for (const range of ranges) {
+              writer.removeAttribute(attribute, range);
+            }
+          });
+        });
+      });
+    }, { priority: 'high' });
+  }
+
+  _refreshExtraAttributeValues() {
+    const editor = this.editor;
+    const attributes = this.attrs
+    const linkCommand = editor.commands.get('link');
+    const model = this.editor.model;
+    const selection = model.document.selection;
+
+    attributes.forEach((attribute) => {
+      linkCommand.set(attribute, null);
+    });
+    model.document.on('change', () => {
+      attributes.forEach((attribute) => {
+        linkCommand[attribute] = selection.getAttribute(attribute);
+      });
+    });
+  }
+
+  /**
+   * @inheritdoc
+   */
+  static get pluginName() {
+    return 'LinkitEditing';
+  }
+}
diff --git a/web/modules/linkit/js/linkit.autocomplete.js b/web/modules/linkit/js/linkit.autocomplete.js
index 8ef722fe6000569b1f114281f85fa40064248cb5..594a2d10e00c61c83e247c65f3839a5d68ffc440 100644
--- a/web/modules/linkit/js/linkit.autocomplete.js
+++ b/web/modules/linkit/js/linkit.autocomplete.js
@@ -3,7 +3,7 @@
  * Linkit Autocomplete based on jQuery UI.
  */
 
-(function ($, Drupal, _) {
+(function ($, Drupal, once) {
 
   'use strict';
 
@@ -63,6 +63,7 @@
    *   False to prevent further handlers.
    */
   function selectHandler(event, ui) {
+    var linkSelector = event.target.getAttribute('data-drupal-selector');
     var $context = $(event.target).closest('form,fieldset,tr');
 
     if (!ui.item.path) {
@@ -83,12 +84,18 @@
 
     if (ui.item.label) {
       // Automatically set the link title.
-      var $linkTitle = $('.linkit-widget-title--autofill-enabled', $context);
+      var $linkTitle = $('*[data-linkit-widget-title-autofill-enabled]', $context);
       if ($linkTitle.length > 0) {
+        var titleSelector = $linkTitle.attr('data-drupal-selector');
+        if (titleSelector === undefined || linkSelector === undefined) {
+          return false;
+        }
+        if (titleSelector.replace('-title', '') !== linkSelector.replace('-uri', '')) {
+          return false;
+        }
         if (!$linkTitle.val() || $linkTitle.hasClass('link-widget-title--auto')) {
           // Set value to the label.
           $linkTitle.val(ui.item.label);
-
           // Flag title as being automatically set.
           $linkTitle.addClass('link-widget-title--auto');
         }
@@ -133,9 +140,14 @@
   function renderMenu(ul, items) {
     var self = this.element.autocomplete('instance');
 
-    var grouped_items = _.groupBy(items, function (item) {
-      return item.hasOwnProperty('group') ? item.group : '';
-    });
+    var grouped_items = {};
+    items.forEach(function (item) {
+      const group = item.hasOwnProperty('group') ? item.group : '';
+      if (!grouped_items.hasOwnProperty(group)) {
+        grouped_items[group] = [];
+      }
+      grouped_items[group].push(item);
+    })
 
     $.each(grouped_items, function (group, items) {
       if (group.length) {
@@ -143,7 +155,9 @@
       }
 
       $.each(items, function (index, item) {
-        self._renderItemData(ul, item);
+        if ( $.isFunction(self._renderItemData) ) {
+          self._renderItemData(ul, item);
+        }
       });
     });
   }
@@ -171,7 +185,7 @@
   Drupal.behaviors.linkit_autocomplete = {
     attach: function (context) {
       // Act on textfields with the "form-linkit-autocomplete" class.
-      var $autocomplete = $(context).find('input.form-linkit-autocomplete').once('linkit-autocomplete');
+      var $autocomplete = $(once('linkit-autocomplete', 'input.form-linkit-autocomplete', context));
       if ($autocomplete.length) {
         $.widget('ui.autocomplete', $.ui.autocomplete, {
           _create: function () {
@@ -182,19 +196,25 @@
           _renderItem: autocomplete.options.renderItem
         });
 
-        // Use jQuery UI Autocomplete on the textfield.
-        $autocomplete.autocomplete(autocomplete.options);
-
-        $autocomplete.click(function () {
-          var $this = $(this);
-          $this.autocomplete('search', $this.val());
-        });
-
         // Process each item.
         $autocomplete.each(function () {
           var $uri = $(this);
+
+          // Use jQuery UI Autocomplete on the textfield.
+          $uri.autocomplete(autocomplete.options);
           $uri.autocomplete('widget').addClass('linkit-ui-autocomplete');
 
+          $uri.click(function () {
+            $uri.autocomplete('search', $uri.val());
+          });
+
+          $uri.on('compositionstart.autocomplete', function () {
+            autocomplete.options.isComposing = true;
+          });
+          $uri.on('compositionend.autocomplete', function () {
+            autocomplete.options.isComposing = false;
+          });
+
           $uri.closest('.form-item').siblings('.form-type-textfield').find('.linkit-widget-title')
             .each(function() {
               // Set automatic title flag if title is the same as uri text.
@@ -209,20 +229,12 @@
               $(this).removeClass('link-widget-title--auto');
             });
         });
-
-        $autocomplete.on('compositionstart.autocomplete', function () {
-          autocomplete.options.isComposing = true;
-        });
-        $autocomplete.on('compositionend.autocomplete', function () {
-          autocomplete.options.isComposing = false;
-        });
       }
     },
     detach: function (context, settings, trigger) {
       if (trigger === 'unload') {
-        $(context).find('input.form-linkit-autocomplete')
-          .removeOnce('linkit-autocomplete')
-          .autocomplete('destroy');
+        once.remove('linkit-autocomplete', 'input.form-linkit-autocomplete', context)
+          .forEach((autocomplete) => $(autocomplete).autocomplete('destroy'));
       }
     }
   };
@@ -247,4 +259,4 @@
     }
   };
 
-})(jQuery, Drupal, _);
+})(jQuery, Drupal, once);
diff --git a/web/modules/linkit/js/linkit.filter_html.admin.js b/web/modules/linkit/js/linkit.filter_html.admin.js
index 3807f6edd17a6ce1411ca166ce6b903b0770c213..8295286fa87320b623a6624739e2b1b518efddf6 100644
--- a/web/modules/linkit/js/linkit.filter_html.admin.js
+++ b/web/modules/linkit/js/linkit.filter_html.admin.js
@@ -3,7 +3,7 @@
  * Send events to add or remove a tags to the filter_html allowed tags.
  */
 
-(function ($, Drupal, document) {
+(function ($, Drupal, document, once) {
 
   'use strict';
 
@@ -19,12 +19,15 @@
     attach: function (context) {
       var selector = '[data-drupal-selector="edit-filters-linkit-status"]';
       var feature = editorFeature();
-      $(context).find(selector).once('filters-linkit-status').each(function () {
-        $(this).on('click', function () {
-          var eventName = $(this).is(':checked') ? 'drupalEditorFeatureAdded' : 'drupalEditorFeatureRemoved';
-          $(document).trigger(eventName, feature);
+
+      once('filters-linkit-status', selector, context)
+        .forEach((checkbox) => {
+          const $checkbox = $(checkbox);
+          $checkbox.on('click', function () {
+            var eventName = $(this).is(':checked') ? 'drupalEditorFeatureAdded' : 'drupalEditorFeatureRemoved';
+            $(document).trigger(eventName, feature);
+          });
         });
-      });
     }
   };
 
@@ -48,4 +51,4 @@
     return linkitFeature;
   }
 
-})(jQuery, Drupal, document);
+})(jQuery, Drupal, document, once);
diff --git a/web/modules/linkit/js/scripts/manifest.js b/web/modules/linkit/js/scripts/manifest.js
new file mode 100644
index 0000000000000000000000000000000000000000..1065ea5bcea6e2014cfaf62940de90c6a4d2b4f8
--- /dev/null
+++ b/web/modules/linkit/js/scripts/manifest.js
@@ -0,0 +1,44 @@
+// CKEditor 5 plugins require a manifest file, which must be generated from
+// CKEditor source, and typically requires several manual steps. That process is
+// automated here.
+
+const fs = require('fs');
+const { exec } = require('child_process');
+
+const manifestPath =
+  './node_modules/ckeditor5/build/ckeditor5-dll.manifest.json';
+
+if (!fs.existsSync(manifestPath)) {
+  console.log(
+    'CKEditor manifest not available. Generating one now. This takes a while, but should only need to happen once.',
+  );
+  exec(
+    'yarn --cwd ./node_modules/ckeditor5 install',
+    (error, stdout, stderr) => {
+      if (error) {
+        console.log(`error: ${error.message}`);
+        return;
+      }
+
+      console.log(stdout);
+      exec(
+        'yarn --cwd ./node_modules/ckeditor5 dll:build',
+        (error, stdout, stderr) => {
+          if (error) {
+            console.log(`error: ${error.message}`);
+            return;
+          }
+
+          console.log(stdout);
+          if (fs.existsSync(manifestPath)) {
+            console.log(`Manifest created at  ${manifestPath}`);
+          } else {
+            console.log('error: Unable to create manifest.');
+          }
+        },
+      );
+    },
+  );
+} else {
+  console.log(`Manifest present at ${manifestPath}`);
+}
diff --git a/web/modules/linkit/linkit.ckeditor5.yml b/web/modules/linkit/linkit.ckeditor5.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e00227863def2618d5ae2637ad10f1aba2ce946
--- /dev/null
+++ b/web/modules/linkit/linkit.ckeditor5.yml
@@ -0,0 +1,15 @@
+linkit_extension:
+  ckeditor5:
+    plugins:
+      - linkit.Linkit
+  drupal:
+    label: Linkit
+    library: linkit/ckeditor5
+    class: Drupal\linkit\Plugin\CKEditor5Plugin\Linkit
+    elements:
+      - <a data-entity-type data-entity-uuid data-entity-substitution>
+    conditions:
+      requiresConfiguration:
+        linkit_enabled: true
+      plugins:
+        - ckeditor5_link
diff --git a/web/modules/linkit/linkit.info.yml b/web/modules/linkit/linkit.info.yml
index b0dd72c307fe9c0154867fbe371319195c5d5045..75caf7533eb8041cac97c466b1444fdd8d66e5f9 100644
--- a/web/modules/linkit/linkit.info.yml
+++ b/web/modules/linkit/linkit.info.yml
@@ -2,12 +2,13 @@ name: Linkit
 type: module
 description: 'Provides an easy interface for internal and external linking with wysiwyg editors.'
 package: User interface
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.4 || <10.1
 configure: entity.linkit_profile.collection
 test_dependencies:
   - imce:imce
+  - drupal:ckeditor
 
-# Information added by Drupal.org packaging script on 2021-09-28
-version: '8.x-5.0-beta13'
+# Information added by Drupal.org packaging script on 2023-07-07
+version: '6.0.0'
 project: 'linkit'
-datestamp: 1632848793
+datestamp: 1688748027
diff --git a/web/modules/linkit/linkit.libraries.yml b/web/modules/linkit/linkit.libraries.yml
index 9e00dd3101bd8c7cc809f7b118b0175fb3c999c0..bb04aedbdebad1b16dfc1159190afdc1ca6bf728 100644
--- a/web/modules/linkit/linkit.libraries.yml
+++ b/web/modules/linkit/linkit.libraries.yml
@@ -23,10 +23,26 @@ linkit.autocomplete:
   dependencies:
     - linkit/linkit.base
     - core/drupal.ajax
-    - core/jquery.ui.autocomplete
-    - core/underscore
+    - core/drupal.autocomplete
+    - core/once
 
 linkit.filter_html.admin:
   version: VERSION
   js:
     js/linkit.filter_html.admin.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/once
+
+ckeditor5:
+  version: VERSION
+  js:
+    js/build/linkit.js: { minified: true }
+  css:
+    component:
+      css/linkit.autocomplete.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal.autocomplete
+    - core/drupal.ajax
diff --git a/web/modules/linkit/linkit.module b/web/modules/linkit/linkit.module
index a20f8520645026f64e94c9841e900d019400b1ff..12dab6127717cf39025242133f3f2970aa08f081 100644
--- a/web/modules/linkit/linkit.module
+++ b/web/modules/linkit/linkit.module
@@ -83,7 +83,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s
 
   $form['href_dirty_check'] = [
     '#type' => 'hidden',
-    '#default_value' => isset($input['href']) ? $input['href'] : '',
+    '#default_value' => $input['href'] ?? '',
   ];
 
   $form['attributes']['href'] = array_merge($form['attributes']['href'], [
@@ -94,7 +94,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s
       'linkit_profile_id' => $linkit_profile_id,
     ],
     "#weight" => -10,
-    '#default_value' => isset($input['href']) ? $input['href'] : '',
+    '#default_value' => $input['href'] ?? '',
   ]);
 
   $fields = [
@@ -109,7 +109,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s
     $form['attributes'][$field_name] = [
       '#title' => $field_name,
       '#type' => 'hidden',
-      '#default_value' => isset($input[$field_name]) ? $input[$field_name] : '',
+      '#default_value' => $input[$field_name] ?? '',
     ];
   }
 
@@ -133,6 +133,7 @@ function linkit_form_editor_link_dialog_submit(array &$form, FormStateInterface
   }
 
   $fields = [
+    'href',
     'data-entity-type',
     'data-entity-uuid',
     'data-entity-substitution',
@@ -149,5 +150,4 @@ function linkit_form_editor_link_dialog_submit(array &$form, FormStateInterface
       }
     }
   }
-
 }
diff --git a/web/modules/linkit/package.json b/web/modules/linkit/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..cb8575c126f9adac4f7eb43510d16ced43b7d5c2
--- /dev/null
+++ b/web/modules/linkit/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "drupal-linkit",
+  "version": "5.0.0",
+  "description": "Provides an easy interface for internal and external linking with wysiwyg editors.",
+  "author": "",
+  "license": "GPL-2.0-or-later",
+  "scripts": {
+    "watch": "yarn manifest && webpack --mode development --watch",
+    "build": "yarn manifest && webpack",
+    "manifest": "node ./js/scripts/manifest.js"
+  },
+  "devDependencies": {
+    "@ckeditor/ckeditor5-dev-utils": "^30.0.0",
+    "ckeditor5": "^35.4.0",
+    "raw-loader": "^4.0.2",
+    "terser-webpack-plugin": "^5.3.3",
+    "webpack": "^5.51.1",
+    "webpack-cli": "^5.0.0"
+  }
+}
diff --git a/web/modules/linkit/src/Element/Linkit.php b/web/modules/linkit/src/Element/Linkit.php
index 2bb7963f62a7462fb8e27fcea20c6163ab05739e..7824dd80b1da366ade8e12799a7cba6279ce3cbf 100644
--- a/web/modules/linkit/src/Element/Linkit.php
+++ b/web/modules/linkit/src/Element/Linkit.php
@@ -58,7 +58,7 @@ public static function processLinkitAutocomplete(&$element, FormStateInterface $
     $access = FALSE;
 
     if (!empty($element['#autocomplete_route_name'])) {
-      $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : [];
+      $parameters = $element['#autocomplete_route_parameters'] ?? [];
       $url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
       /** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
       $access_manager = \Drupal::service('access_manager');
diff --git a/web/modules/linkit/src/Entity/Profile.php b/web/modules/linkit/src/Entity/Profile.php
index 070e98472ab8b23a7285e7a04f3b3f2bad65d48f..ee31f47547830a47733ebdabae79e12c2b741b7b 100644
--- a/web/modules/linkit/src/Entity/Profile.php
+++ b/web/modules/linkit/src/Entity/Profile.php
@@ -35,8 +35,8 @@
  *     "delete-form" = "/admin/config/content/linkit/manage/{linkit_profile}/delete"
  *   },
  *   config_export = {
- *     "id",
  *     "label",
+ *     "id",
  *     "description",
  *     "matchers"
  *   }
diff --git a/web/modules/linkit/src/MatcherBase.php b/web/modules/linkit/src/MatcherBase.php
index 1176e084ffb6b7fca81dee3274d31a2f09803b27..2f2a74a4cd3061e30b162c85afb9d4b392301d00 100644
--- a/web/modules/linkit/src/MatcherBase.php
+++ b/web/modules/linkit/src/MatcherBase.php
@@ -87,10 +87,10 @@ public function setWeight($weight) {
    */
   public function getConfiguration() {
     return [
-      'uuid' => $this->getUuid(),
       'id' => $this->getPluginId(),
-      'weight' => $this->getWeight(),
+      'uuid' => $this->getUuid(),
       'settings' => $this->configuration,
+      'weight' => $this->getWeight(),
     ];
   }
 
diff --git a/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php b/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd6062324ff2798b39ed82f0e65235e4bf25bffe
--- /dev/null
+++ b/web/modules/linkit/src/Plugin/CKEditor4To5Upgrade/Linkit.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\linkit\Plugin\CKEditor4To5Upgrade;
+
+use Drupal\ckeditor5\HTMLRestrictions;
+use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\filter\FilterFormatInterface;
+
+/**
+ * Provides the CKEditor 4 to 5 upgrade for Linkit's CKEditor plugin.
+ *
+ * @CKEditor4To5Upgrade(
+ *   id = "linkit",
+ *   cke4_plugin_settings = {
+ *     "drupallink",
+ *   }
+ * )
+ */
+class Linkit extends PluginBase implements CKEditor4To5UpgradePluginInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mapCKEditor4ToolbarButtonToCKEditor5ToolbarItem(string $cke4_button, HTMLRestrictions $text_format_html_restrictions): ?array {
+    throw new \OutOfBoundsException();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mapCKEditor4SettingsToCKEditor5Configuration(string $cke4_plugin_id, array $cke4_plugin_settings): ?array {
+    switch ($cke4_plugin_id) {
+      // @see \Drupal\linkit\Plugin\CKEditorPlugin\LinkitDrupalLink
+      // @see \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit
+      case 'drupallink':
+        $sanitized = [];
+        if (!isset($cke4_plugin_settings['linkit_enabled']) || !isset($cke4_plugin_settings['linkit_profile'])) {
+          $sanitized['linkit_enabled'] = FALSE;
+        }
+        else {
+          $sanitized['linkit_enabled'] = (bool) $cke4_plugin_settings['linkit_enabled'];
+          if ($sanitized['linkit_enabled']) {
+            $sanitized['linkit_profile'] = $cke4_plugin_settings['linkit_profile'];
+          }
+        }
+        return ['linkit_extension' => $sanitized];
+
+      default:
+        throw new \OutOfBoundsException();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function computeCKEditor5PluginSubsetConfiguration(string $cke5_plugin_id, FilterFormatInterface $text_format): ?array {
+    throw new \OutOfBoundsException();
+  }
+
+}
diff --git a/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php b/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php
new file mode 100644
index 0000000000000000000000000000000000000000..a0cc779793888c082b0e2cdf84edf2f6ef1c9151
--- /dev/null
+++ b/web/modules/linkit/src/Plugin/CKEditor5Plugin/Linkit.php
@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\linkit\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\editor\EditorInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+
+/**
+ * CKEditor 5 Linkit plugin configuration.
+ */
+class Linkit extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, ContainerFactoryPluginInterface {
+
+  use CKEditor5PluginConfigurableTrait;
+
+  /**
+   * The Linkit profile storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $linkitProfileStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $linkit_profile_storage) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->linkitProfileStorage = $linkit_profile_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager')->getStorage('linkit_profile')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $all_profiles = $this->linkitProfileStorage->loadMultiple();
+
+    $options = [];
+    foreach ($all_profiles as $profile) {
+      $options[$profile->id()] = $profile->label();
+    }
+
+    $form['linkit_profile'] = [
+      '#wrapper_attributes' => ['class' => ['container-inline']],
+      '#type' => 'select',
+      '#title' => $this->t('Linkit profile'),
+      '#options' => $options,
+      '#default_value' => $this->configuration['linkit_profile'] ?? '',
+      '#empty_option' => $this->t('- Linkit disabled -'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Config validation callback: require linkit_profile if linkit_enabled=TRUE.
+   *
+   * @param array $values
+   *   The configuration subtree for ckeditor5.plugin.linkit_extension.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validation execution context.
+   *
+   * @see linkit.schema.yml
+   */
+  public static function requireProfileIfEnabled(array $values, ExecutionContextInterface $context): void {
+    if ($values['linkit_enabled'] === TRUE && empty($values['linkit_profile'])) {
+      $context->buildViolation(t('Linkit is enabled, please select the Linkit profile you wish to use.'))
+        ->atPath('linkit_profile')
+        ->addViolation();
+    }
+    elseif ($values['linkit_enabled'] === FALSE && !empty($values['linkit_profile'])) {
+      $context->buildViolation(t('Linkit is disabled; it does not make sense to associate a Linkit profile.'))
+        ->atPath('linkit_profile')
+        ->addViolation();
+    }
+  }
+
+  /**
+   * Computes all valid choices for the "linkit_profile" setting.
+   *
+   * @see linkit.schema.yml
+   *
+   * @return string[]
+   *   All valid choices.
+   */
+  public static function validChoices(): array {
+    $linkit_profile_storage = \Drupal::service('entity_type.manager')->getStorage('linkit_profile');
+    assert($linkit_profile_storage instanceof EntityStorageInterface);
+    return array_keys($linkit_profile_storage->loadMultiple());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Match the config schema structure at ckeditor5.plugin.linkit_extension.
+    if (empty($form_state->getValue('linkit_profile'))) {
+      $form_state->unsetValue('linkit_profile');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['linkit_enabled'] = $form_state->hasValue('linkit_profile');
+    // `linkit_profile` only is relevant when Linkit is enabled.
+    if ($this->configuration['linkit_enabled']) {
+      $this->configuration['linkit_profile'] = $form_state->getValue('linkit_profile');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'linkit_enabled' => FALSE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
+    assert($this->configuration['linkit_enabled'] === TRUE);
+    return [
+      'linkit' => [
+        'profile' => $this->configuration['linkit_profile'],
+        'autocompleteUrl' => Url::fromRoute('linkit.autocomplete', ['linkit_profile_id' => $this->configuration['linkit_profile']])
+          ->toString(TRUE)
+          ->getGeneratedUrl()
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php b/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php
index c0168bcf8ae7af2469c6ad51d4f6fb01225ece28..1b4c24f0b7a53bb4065c3d1392e7c33d27ad6e1a 100644
--- a/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php
+++ b/web/modules/linkit/src/Plugin/CKEditorPlugin/LinkitDrupalLink.php
@@ -58,7 +58,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
     $form['linkit_enabled'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Linkit enabled'),
-      '#default_value' => isset($settings['plugins']['drupallink']['linkit_enabled']) ? $settings['plugins']['drupallink']['linkit_enabled'] : '',
+      '#default_value' => $settings['plugins']['drupallink']['linkit_enabled'] ?? '',
       '#description' => $this->t('Enable Linkit for this text format.'),
     ];
 
@@ -66,7 +66,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
       '#type' => 'select',
       '#title' => $this->t('Linkit profile'),
       '#options' => $options,
-      '#default_value' => isset($settings['plugins']['drupallink']['linkit_profile']) ? $settings['plugins']['drupallink']['linkit_profile'] : '',
+      '#default_value' => $settings['plugins']['drupallink']['linkit_profile'] ?? '',
       '#empty_option' => $this->t('- Select -'),
       '#description' => $this->t('Select the Linkit profile you wish to use with this text format.'),
       '#states' => [
diff --git a/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php b/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php
index c1070349b7f980a706c4a7a09346f417cedc9041..87e40f752672548fed93c1ae5a515c988c74390e 100644
--- a/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php
+++ b/web/modules/linkit/src/Plugin/Field/FieldFormatter/LinkitFormatter.php
@@ -2,12 +2,16 @@
 
 namespace Drupal\linkit\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Path\PathValidatorInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\link\LinkItemInterface;
 use Drupal\link\Plugin\Field\FieldFormatter\LinkFormatter;
-use Drupal\linkit\Entity\Profile;
+use Drupal\linkit\ProfileInterface;
 use Drupal\linkit\SubstitutionManagerInterface;
 use Drupal\linkit\Utility\LinkitHelper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -33,20 +37,59 @@ class LinkitFormatter extends LinkFormatter {
   protected $substitutionManager;
 
   /**
-   * The entity type manager.
+   * The linkit profile storage service.
    *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   * @var \Drupal\Core\Entity\EntityStorageInterface
    */
-  protected $entityTypeManager;
+  protected $linkitProfileStorage;
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
-    $instance->substitutionManager = $container->get('plugin.manager.linkit.substitution');
-    $instance->entityTypeManager = $container->get('entity_type.manager');
-    return $instance;
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('path.validator'),
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.linkit.substitution')
+    );
+  }
+
+  /**
+   * Constructs a new Linkit field formatter.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Third party settings.
+   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
+   *   The path validator service.
+   * @param Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager service.
+   * @param \Drupal\linkit\SubstitutionManagerInterface $substitution_manager
+   *   The substitution manager.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, PathValidatorInterface $path_validator, EntityTypeManagerInterface $entityTypeManager, SubstitutionManagerInterface $substitution_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $path_validator);
+
+    $this->substitutionManager = $substitution_manager;
+    $this->linkitProfileStorage = $entityTypeManager->getStorage('linkit_profile');
   }
 
   /**
@@ -64,12 +107,9 @@ public static function defaultSettings() {
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $elements = parent::settingsForm($form, $form_state);
 
-    $linkit_profiles = $this->entityTypeManager->getStorage('linkit_profile')->loadMultiple();
-
-    $options = [];
-    foreach ($linkit_profiles as $linkit_profile) {
-      $options[$linkit_profile->id()] = $linkit_profile->label();
-    }
+    $options = array_map(function ($linkit_profile) {
+      return $linkit_profile->label();
+    }, $this->linkitProfileStorage->loadMultiple());
 
     $elements['linkit_profile'] = [
       '#type' => 'select',
@@ -89,7 +129,7 @@ public function settingsSummary() {
     $summary = parent::settingsSummary();
 
     $linkit_profile_id = $this->getSetting('linkit_profile');
-    $linkit_profile = $this->entityTypeManager->getStorage('linkit_profile')->load($linkit_profile_id);
+    $linkit_profile = $this->linkitProfileStorage->load($linkit_profile_id);
 
     if ($linkit_profile) {
       $summary[] = $this->t('Linkit profile: @linkit_profile', ['@linkit_profile' => $linkit_profile->label()]);
@@ -110,7 +150,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
       $link_item = $items->get($delta);
       $substituted_url = $this->getSubstitutedUrl($link_item);
       // Convert generated URL into a URL object.
-      if ($substituted_url && ($url = \Drupal::pathValidator()->getUrlIfValid($substituted_url->getGeneratedUrl()))) {
+      if ($substituted_url && ($url = $this->pathValidator->getUrlIfValid($substituted_url->getGeneratedUrl()))) {
         // Keep query and fragment.
         $parsed_url = parse_url($link_item->uri);
         if (!empty($parsed_url['query'])) {
@@ -151,15 +191,26 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    *   The substitution URL, or NULL if not able to retrieve it from the item.
    */
   protected function getSubstitutedUrl(LinkItemInterface $item) {
-    if ($entity = LinkitHelper::getEntityFromUserInput($item->uri)) {
-      $profile = Profile::load($this->getSettings()['linkit_profile']);
+    // First try to derive entity information from Linkit-specific attributes.
+    // This is more reliable and is required for File entities.
+    if (!empty($item->options['data-entity-type']) && !empty($item->options['data-entity-uuid'])) {
+      $entity = \Drupal::service('entity.repository')->loadEntityByUuid($item->options['data-entity-type'], $item->options['data-entity-uuid']);
+    }
+    else {
+      $entity = LinkitHelper::getEntityFromUserInput($item->uri);
+    }
+    if ($entity instanceof EntityInterface) {
+      $linkit_profile = $this->linkitProfileStorage->load($this->getSettings()['linkit_profile']);
 
-      /** @var \\Drupal\linkit\Plugin\Linkit\Matcher\EntityMatcher $matcher */
-      $matcher = $profile->getMatcherByEntityType($entity->getEntityTypeId());
+      if (!$linkit_profile instanceof ProfileInterface) {
+        return NULL;
+      }
+
+      /** @var \Drupal\linkit\Plugin\Linkit\Matcher\EntityMatcher $matcher */
+      $matcher = $linkit_profile->getMatcherByEntityType($entity->getEntityTypeId());
       $substitution_type = $matcher ? $matcher->getConfiguration()['settings']['substitution_type'] : SubstitutionManagerInterface::DEFAULT_SUBSTITUTION;
       return $this->substitutionManager->createInstance($substitution_type)->getUrl($entity);
     }
-
     return NULL;
   }
 
diff --git a/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php b/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php
index f67867e7e524c3701076ddbbec03741699e6c4e7..c9e46203144be5b31fd6657a57ee446bdd33cd1f 100644
--- a/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php
+++ b/web/modules/linkit/src/Plugin/Field/FieldWidget/LinkitWidget.php
@@ -2,11 +2,16 @@
 
 namespace Drupal\linkit\Plugin\Field\FieldWidget;
 
-use Drupal\Core\Url;
-use Drupal\link\Plugin\Field\FieldWidget\LinkWidget;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\file\FileInterface;
+use Drupal\link\Plugin\Field\FieldWidget\LinkWidget;
 use Drupal\linkit\Utility\LinkitHelper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Plugin implementation of the 'linkit' widget.
@@ -22,118 +27,69 @@
 class LinkitWidget extends LinkWidget {
 
   /**
-   * {@inheritdoc}
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
    */
-  public static function defaultSettings() {
-    return [
-      'linkit_profile' => 'default',
-      'linkit_auto_link_text' => FALSE,
-    ] + parent::defaultSettings();
-  }
+  protected $currentUser;
 
   /**
-   * {@inheritdoc}
+   * The linkit profile storage service.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
    */
-  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
-    $item = $items[$delta];
-    $uri = $item->uri;
-    $uri_scheme = parse_url($uri, PHP_URL_SCHEME);
-    $is_nolink = substr($uri, 0, 14) === 'route:<nolink>';
-    if (!empty($uri) && empty($uri_scheme) && $is_nolink) {
-      $uri = LinkitHelper::uriFromUserInput($uri);
-      $uri_scheme = parse_url($uri, PHP_URL_SCHEME);
-    }
-    if ($is_nolink) {
-      $uri_as_url = $uri;
-    }
-    else {
-      $uri_as_url = !empty($uri) ? static::getUriAsDisplayableString($uri) : '';
-    }
-    $linkit_profile_id = $this->getSetting('linkit_profile');
-
-    // The current field value could have been entered by a different user.
-    // However, if it is inaccessible to the current user, do not display it
-    // to them.
-    $default_allowed = !$item->isEmpty() && (\Drupal::currentUser()->hasPermission('link to any page') || $item->getUrl()->access());
-
-    if ($default_allowed && $uri_scheme == 'entity') {
-      $entity = LinkitHelper::getEntityFromUri($uri);
-    }
-
-    $element['uri'] = [
-      '#type' => 'linkit',
-      '#title' => $this->t('URL'),
-      '#placeholder' => $this->getSetting('placeholder_url'),
-      '#default_value' => $default_allowed ? $uri_as_url : NULL,
-      '#maxlength' => 2048,
-      '#required' => $element['#required'],
-      '#description' => $this->t('Start typing to find content or paste a URL and click on the suggestion below.'),
-      '#autocomplete_route_name' => 'linkit.autocomplete',
-      '#autocomplete_route_parameters' => [
-        'linkit_profile_id' => $linkit_profile_id,
-      ],
-      '#error_no_message' => TRUE,
-    ];
-
-    $element['attributes']['href'] = [
-      '#type' => 'hidden',
-      '#default_value' => $default_allowed ? $uri : '',
-    ];
-
-    $element['attributes']['data-entity-type'] = [
-      '#type' => 'hidden',
-      '#default_value' => $default_allowed && isset($entity) ? $entity->getEntityTypeId() : '',
-    ];
-
-    $element['attributes']['data-entity-uuid'] = [
-      '#type' => 'hidden',
-      '#default_value' => $default_allowed && isset($entity) ? $entity->uuid() : '',
-    ];
-
-    $element['attributes']['data-entity-substitution'] = [
-      '#type' => 'hidden',
-      '#default_value' => $default_allowed && isset($entity) ? $entity->getEntityTypeId() == 'file' ? 'file' : 'canonical' : '',
-    ];
+  protected $linkitProfileStorage;
 
-    $element['title'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Link text'),
-      '#placeholder' => $this->getSetting('placeholder_title'),
-      '#default_value' => isset($items[$delta]->title) ? $items[$delta]->title : NULL,
-      '#maxlength' => 255,
-      '#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED,
-      '#required' => $this->getFieldSetting('title') === DRUPAL_REQUIRED && $element['#required'],
-      '#attributes' => [
-        'class' => ['linkit-widget-title'],
-      ],
-      '#error_no_message' => TRUE,
-    ];
-    if ($this->getSetting('linkit_auto_link_text')) {
-      $element['title']['#attributes']['class'][] = 'linkit-widget-title--autofill-enabled';
-    }
-    // Post-process the title field to make it conditionally required if URL is
-    // non-empty. Omit the validation on the field edit form, since the field
-    // settings cannot be saved otherwise.
-    if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') == DRUPAL_REQUIRED) {
-      $element['#element_validate'][] = [get_called_class(), 'validateTitleElement'];
-    }
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('current_user'),
+      $container->get('entity_type.manager')
+    );
+  }
 
-    // If cardinality is 1, ensure a proper label is output for the field.
-    if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) {
-      // If the link title is disabled, use the field definition label as the
-      // title of the 'uri' element.
-      if ($this->getFieldSetting('title') == DRUPAL_DISABLED) {
-        $element['uri']['#title'] = $element['#title'];
-      }
-      // Otherwise wrap everything in a details element.
-      else {
-        $element += [
-          '#type' => 'fieldset',
-        ];
-      }
-    }
+  /**
+   * Constructs a new Linkit field widget.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The widget settings.
+   * @param array $third_party_settings
+   *   The widget third party settings.
+   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager service.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountProxyInterface $currentUser, EntityTypeManagerInterface $entityTypeManager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+    $this->currentUser = $currentUser;
+    $this->linkitProfileStorage = $entityTypeManager->getStorage('linkit_profile');
+  }
 
-    return $element;
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'linkit_profile' => 'default',
+      'linkit_auto_link_text' => FALSE,
+    ] + parent::defaultSettings();
   }
 
   /**
@@ -142,12 +98,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $elements = parent::settingsForm($form, $form_state);
 
-    $linkit_profiles = \Drupal::entityTypeManager()->getStorage('linkit_profile')->loadMultiple();
-
-    $options = [];
-    foreach ($linkit_profiles as $linkit_profile) {
-      $options[$linkit_profile->id()] = $linkit_profile->label();
-    }
+    $options = array_map(function ($linkit_profile) {
+      return $linkit_profile->label();
+    }, $this->linkitProfileStorage->loadMultiple());
 
     $elements['linkit_profile'] = [
       '#type' => 'select',
@@ -171,7 +124,7 @@ public function settingsSummary() {
     $summary = parent::settingsSummary();
 
     $linkit_profile_id = $this->getSetting('linkit_profile');
-    $linkit_profile = \Drupal::entityTypeManager()->getStorage('linkit_profile')->load($linkit_profile_id);
+    $linkit_profile = $this->linkitProfileStorage->load($linkit_profile_id);
 
     if ($linkit_profile) {
       $summary[] = $this->t('Linkit profile: @linkit_profile', ['@linkit_profile' => $linkit_profile->label()]);
@@ -189,28 +142,84 @@ public function settingsSummary() {
   /**
    * {@inheritdoc}
    */
-  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
-    foreach ($values as &$value) {
-      $value['uri'] = LinkitHelper::uriFromUserInput($value['uri']);
-      $value += ['options' => []];
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    /** @var \Drupal\link\LinkItemInterface $item */
+    $item = $items[$delta];
+    $uri = $item->uri ?? NULL;
+
+    // Try to fetch entity information from the URI.
+    $default_allowed = !$item->isEmpty() && ($this->currentUser->hasPermission('link to any page') || $item->getUrl()->access());
+    if (!empty($item->options['data-entity-type']) && !empty($item->options['data-entity-uuid'])) {
+      $entity = \Drupal::service('entity.repository')->loadEntityByUuid($item->options['data-entity-type'], $item->options['data-entity-uuid']);
     }
-    return $values;
+    else {
+      $entity = $default_allowed && $uri ? LinkitHelper::getEntityFromUri($uri) : NULL;
+    }
+    // Display entity URL consistently across all entity types.
+    if ($entity instanceof FileInterface) {
+      // File entities are anomalies, so we handle them differently.
+      $element['uri']['#default_value'] = \Drupal::service('file_url_generator')->generateString($entity->getFileUri());
+    }
+    elseif ($entity instanceof EntityInterface) {
+      $uri_parts = parse_url($uri);
+      $uri_options = [];
+      // Extract query parameters and fragment and merge them into $uri_options.
+      if (isset($uri_parts['fragment']) && $uri_parts['fragment'] !== '') {
+        $uri_options += ['fragment' => $uri_parts['fragment']];
+      }
+      if (!empty($uri_parts['query'])) {
+        $uri_query = [];
+        parse_str($uri_parts['query'], $uri_query);
+        $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query;
+      }
+      $element['uri']['#default_value'] = $entity->toUrl()->setOptions($uri_options)->toString();
+    }
+    // Change the URI field to use the linkit profile.
+    $element['uri']['#type'] = 'linkit';
+    $element['uri']['#description'] = $this->t('Start typing to find content or paste a URL and click on the suggestion below.');
+    $element['uri']['#autocomplete_route_name'] = 'linkit.autocomplete';
+    $element['uri']['#autocomplete_route_parameters'] = [
+      'linkit_profile_id' => $this->getSetting('linkit_profile'),
+    ];
+
+    // Add a class to the title field.
+    $element['title']['#attributes']['class'][] = 'linkit-widget-title';
+    if ($this->getSetting('linkit_auto_link_text')) {
+      $element['title']['#attributes']['data-linkit-widget-title-autofill-enabled'] = TRUE;
+    }
+
+    // Add linkit specific attributes.
+    $element['attributes']['href'] = [
+      '#type' => 'hidden',
+      '#default_value' => $default_allowed ? $uri : '',
+    ];
+    $element['attributes']['data-entity-type'] = [
+      '#type' => 'hidden',
+      '#default_value' => $entity ? $entity->getEntityTypeId() : '',
+    ];
+    $element['attributes']['data-entity-uuid'] = [
+      '#type' => 'hidden',
+      '#default_value' => $entity ? $entity->uuid() : '',
+    ];
+    $element['attributes']['data-entity-substitution'] = [
+      '#type' => 'hidden',
+      '#default_value' => $entity ? ($entity->getEntityTypeId() === 'file' ? 'file' : 'canonical') : '',
+    ];
+
+    return $element;
   }
 
   /**
    * {@inheritdoc}
    */
-  protected static function getUriAsDisplayableString($uri) {
-    $scheme = parse_url($uri, PHP_URL_SCHEME);
-    if ($scheme === 'base') {
-      $uri_reference = explode(':', $uri, 2)[1];
-      $uri = 'internal:' . $uri_reference;
-    }
-    elseif ($scheme === 'entity') {
-      $uri_reference = explode(':', $uri, 2)[1];
-      $uri = '/' . $uri_reference;
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    foreach ($values as &$value) {
+      $value['uri'] = LinkitHelper::uriFromUserInput($value['uri']);
+      $value += ['options' => $value['attributes']];
     }
-    return parent::getUriAsDisplayableString($uri);
+    return $values;
   }
 
 }
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php
index d6ea3613069e0f6446c58f7598968097ffcb3f7b..c9513d94d04f7cd0bb58273b5c252a7c2525145b 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/EmailMatcher.php
@@ -23,14 +23,11 @@ class EmailMatcher extends MatcherBase {
   public function execute($string) {
     $suggestions = new SuggestionCollection();
 
-    // Strip the mailto: prefix to match only the e-mail part of the string.
-    $string = str_replace('mailto:', '', $string);
-
     // Check for an e-mail address then return an e-mail match and create a
     // mail-to link if appropriate.
     if (filter_var($string, FILTER_VALIDATE_EMAIL)) {
       $suggestion = new DescriptionSuggestion();
-      $suggestion->setLabel($string)
+      $suggestion->setLabel($this->t('E-mail @email', ['@email' => $string]))
         ->setPath('mailto:' . Html::escape($string))
         ->setGroup($this->t('E-mail'))
         ->setDescription($this->t('Opens your mail client ready to e-mail @email', ['@email' => $string]));
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php
index db46595308b70ed8432ef24f21e91f3f0d654c1b..e48c6afb9e3618e56400f3e0f656683af56cd8ce 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/EntityMatcher.php
@@ -322,6 +322,7 @@ public static function elementValidateFilter(&$element, FormStateInterface $form
   public function execute($string) {
     $suggestions = new SuggestionCollection();
     $query = $this->buildEntityQuery($string);
+    $query->accessCheck(TRUE);
     $query_result = $query->execute();
     $url_results = $this->findEntityIdByUrl($string);
     $result = array_merge($query_result, $url_results);
@@ -344,12 +345,6 @@ public function execute($string) {
 
       $entity = $this->entityRepository->getTranslationFromContext($entity);
       $suggestion = $this->createSuggestion($entity);
-      if ($query = parse_url($string, PHP_URL_QUERY)) {
-        $suggestion->setPath($suggestion->getPath() . '?' . $query);
-      }
-      if ($fragment = parse_url($string, PHP_URL_FRAGMENT)) {
-        $suggestion->setPath($suggestion->getPath() . '#' . $fragment);
-      }
       $suggestions->addSuggestion($suggestion);
     }
 
@@ -371,6 +366,7 @@ protected function buildEntityQuery($search_string) {
 
     $entity_type = $this->entityTypeManager->getDefinition($this->targetType);
     $query = $this->entityTypeManager->getStorage($this->targetType)->getQuery();
+    $query->accessCheck(TRUE);
     $label_key = $entity_type->getKey('label');
 
     if ($label_key) {
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php
index 9d05089982a2c81c44be38f99e6b9777a33ae35d..cc0fba30f427aa7060e74ad9cc60967a256c5049 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/FileMatcher.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\file\FileInterface;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\linkit\Utility\LinkitXss;
 
@@ -57,7 +58,7 @@ public function getSummary() {
   public function defaultConfiguration() {
     return [
       'file_extensions' => '',
-      'file_status' => FILE_STATUS_PERMANENT,
+      'file_status' => FileInterface::STATUS_PERMANENT,
       'images' => [
         'show_dimensions' => FALSE,
         'show_thumbnail' => FALSE,
@@ -226,7 +227,7 @@ protected function buildDescription(EntityInterface $entity) {
    */
   protected function buildPath(EntityInterface $entity) {
     /** @var \Drupal\file\FileInterface $entity */
-    return file_url_transform_relative(file_create_url($entity->getFileUri()));
+    return \Drupal::service('file_url_generator')->generateString($entity->getFileUri());
   }
 
 }
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php
index 90347b6ed61aaa68c8710e0422338ed9d8d1a0a6..dd3ca15eaac87e86e15ac64a880bdfe848517988 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/FrontPageMatcher.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\linkit\Plugin\Linkit\Matcher;
 
+use Drupal\Core\Url;
 use Drupal\linkit\MatcherBase;
 use Drupal\linkit\Suggestion\DescriptionSuggestion;
 use Drupal\linkit\Suggestion\SuggestionCollection;
-use Drupal\linkit\Utility\LinkitHelper;
 
 /**
  * Provides specific linkit matchers for the front page.
@@ -22,18 +22,12 @@ class FrontPageMatcher extends MatcherBase {
    */
   public function execute($string) {
     $suggestions = new SuggestionCollection();
-    $front_path = '/';
-    $query_and_fragment = LinkitHelper::getQueryAndFragment($string);
-
-    if (!empty($query_and_fragment)) {
-      $string = substr($string, 0, strpos($string, $query_and_fragment));
-    }
 
     // Special for link to front page.
-    if (strpos($string, 'front') !== FALSE || $string == $front_path) {
+    if (strpos($string, 'front') !== FALSE) {
       $suggestion = new DescriptionSuggestion();
       $suggestion->setLabel($this->t('Front page'))
-        ->setPath($front_path . $query_and_fragment)
+        ->setPath(Url::fromRoute('<front>')->toString())
         ->setGroup($this->t('System'))
         ->setDescription($this->t('The front page for this site.'));
 
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php
index f40c7c006e049a7fbaeb92b579ccdbbbd004f9e1..b09854622766ded57c24772453e86702f693acff 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Matcher/NodeMatcher.php
@@ -88,7 +88,7 @@ protected function buildEntityQuery($search_string) {
     if ($this->configuration['include_unpublished'] == FALSE) {
       $query->condition('status', NodeInterface::PUBLISHED);
     }
-    elseif (count($this->moduleHandler->getImplementations('node_grants')) === 0) {
+    elseif (!$this->moduleHandler->hasImplementations('node_grants')) {
       if (($this->currentUser->hasPermission('bypass node access') || $this->currentUser->hasPermission('view any unpublished content'))) {
         // User can see all content, no check necessary.
       }
diff --git a/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php b/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php
deleted file mode 100644
index 90eee020b40ee45e700b38f54527c2d9f47f9acb..0000000000000000000000000000000000000000
--- a/web/modules/linkit/src/Plugin/Linkit/Matcher/NolinkMatcher.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Drupal\linkit\Plugin\Linkit\Matcher;
-
-use Drupal\linkit\MatcherBase;
-use Drupal\linkit\Suggestion\DescriptionSuggestion;
-use Drupal\linkit\Suggestion\SuggestionCollection;
-
-/**
- * Provides a linkit matcher for route:<nolink>.
- *
- * @Matcher(
- *   id = "nolink",
- *   label = @Translation("Nolink"),
- * )
- */
-class NolinkMatcher extends MatcherBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($string) {
-    $suggestions = new SuggestionCollection();
-
-    // Check for the text 'nolink' (e.g. like route:<nolink> with core link
-    // fields) and return route:<nolink> if it exists.
-    if (strpos($string, 'nolink') !== FALSE) {
-      $suggestion = new DescriptionSuggestion();
-      $suggestion->setLabel($this->t('Empty link'))
-        ->setPath('route:<nolink>')
-        ->setGroup($this->t('System'))
-        ->setDescription($this->t('An empty link'));
-
-      $suggestions->addSuggestion($suggestion);
-    }
-    return $suggestions;
-  }
-
-}
diff --git a/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php b/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php
index c441dfc1af22a5a75bfeaa99dd108655e483d217..cfffe04e7920126b34f8b5c85bf8b1e30e6fc767 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Substitution/File.php
@@ -24,7 +24,7 @@ class File extends PluginBase implements SubstitutionInterface {
   public function getUrl(EntityInterface $entity) {
     $url = new GeneratedUrl();
     /** @var \Drupal\file\FileInterface $entity */
-    $url->setGeneratedUrl(file_create_url($entity->getFileUri()));
+    $url->setGeneratedUrl($entity->createFileUrl());
     $url->addCacheableDependency($entity);
     return $url;
   }
diff --git a/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php b/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php
index 9cdcb1cead2718470835d9950fec152046f5b880..bdcf1ab36925f7231dcb036eb452914716d2cb6a 100644
--- a/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php
+++ b/web/modules/linkit/src/Plugin/Linkit/Substitution/Media.php
@@ -48,7 +48,7 @@ public function getUrl(EntityInterface $entity) {
     if ($source_field && $entity->hasField($source_field->getName()) && $entity->get($source_field->getName())->entity instanceof FileInterface) {
       /** @var \Drupal\file\FileInterface $file */
       $file = $entity->get($source_field->getName())->entity;
-      $url->setGeneratedUrl(file_create_url($file->getFileUri()));
+      $url->setGeneratedUrl($file->createFileUrl());
       $url->addCacheableDependency($entity);
       return $url;
     }
diff --git a/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php b/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php
index 30d6ad0cfc9eaaa4dc9f88c432298af70353a2cd..2fe03d8b5077a32b00e474f13a082d232e88a7a7 100644
--- a/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php
+++ b/web/modules/linkit/src/Suggestion/DescriptionSuggestion.php
@@ -34,6 +34,7 @@ public function setDescription($description) {
   /**
    * {@inheritdoc}
    */
+  #[\ReturnTypeWillChange]
   public function jsonSerialize() {
 
     return parent::jsonSerialize() + [
diff --git a/web/modules/linkit/src/Suggestion/EntitySuggestion.php b/web/modules/linkit/src/Suggestion/EntitySuggestion.php
index 772db110f116cb616999f826ad96b076ffe962f8..bd55db3c4e7e45834712eee42d85ab4c9a9284da 100644
--- a/web/modules/linkit/src/Suggestion/EntitySuggestion.php
+++ b/web/modules/linkit/src/Suggestion/EntitySuggestion.php
@@ -70,6 +70,7 @@ public function setSubstitutionId($substitution_id) {
   /**
    * {@inheritdoc}
    */
+  #[\ReturnTypeWillChange]
   public function jsonSerialize() {
     return parent::jsonSerialize() + [
       'entity_uuid' => $this->entityUuid,
diff --git a/web/modules/linkit/src/Suggestion/SimpleSuggestion.php b/web/modules/linkit/src/Suggestion/SimpleSuggestion.php
index 49063906d75c365b598d941f86e2560bc1e84493..db278801e21dc01c9f820ecae26e4e37b5e7c6bf 100644
--- a/web/modules/linkit/src/Suggestion/SimpleSuggestion.php
+++ b/web/modules/linkit/src/Suggestion/SimpleSuggestion.php
@@ -76,6 +76,7 @@ public function setGroup($group) {
   /**
    * {@inheritdoc}
    */
+  #[\ReturnTypeWillChange]
   public function jsonSerialize() {
     return [
       'label' => $this->getLabel(),
diff --git a/web/modules/linkit/src/Suggestion/SuggestionCollection.php b/web/modules/linkit/src/Suggestion/SuggestionCollection.php
index de20548a37d14870bbb62391f288042fb4a5f86b..9117009e6e7239bffa688362512800c9253532e7 100644
--- a/web/modules/linkit/src/Suggestion/SuggestionCollection.php
+++ b/web/modules/linkit/src/Suggestion/SuggestionCollection.php
@@ -47,6 +47,7 @@ public function addSuggestions(SuggestionCollection $suggestionCollection) {
   /**
    * {@inheritdoc}
    */
+  #[\ReturnTypeWillChange]
   public function jsonSerialize() {
     return [
       'suggestions' => $this->suggestions,
diff --git a/web/modules/linkit/src/SuggestionManager.php b/web/modules/linkit/src/SuggestionManager.php
index 82004c594ecfa5ba509cc2cb8a5cb7764e34b9c4..1f7b28084bb8ceb05861c16b5ec1025a724dfc0e 100644
--- a/web/modules/linkit/src/SuggestionManager.php
+++ b/web/modules/linkit/src/SuggestionManager.php
@@ -54,7 +54,7 @@ public function addUnscathedSuggestion(SuggestionCollection $suggestionCollectio
     $suggestion = new DescriptionSuggestion();
     $suggestion->setLabel(Html::escape($search_string))
       ->setGroup($this->t('No results'))
-      ->setDescription($this->t('Linkit could not find any suggestions. This URL will be used as is.'))
+      ->setDescription($this->t('No content suggestions found. This URL will be used as is.'))
       ->setPath($search_string);
     $suggestionCollection->addSuggestion($suggestion);
     return $suggestionCollection;
diff --git a/web/modules/linkit/src/Utility/LinkitHelper.php b/web/modules/linkit/src/Utility/LinkitHelper.php
index 8376ca4048a3e2f1190eccfeae8a517b981537aa..e1ba0f7f4d6c34e05b8ba659cbec1e2ca1de756b 100644
--- a/web/modules/linkit/src/Utility/LinkitHelper.php
+++ b/web/modules/linkit/src/Utility/LinkitHelper.php
@@ -23,11 +23,12 @@ class LinkitHelper {
    *   uri, or NULL if could not match any entity.
    */
   public static function getEntityFromUri($uri) {
-    // Stripe out potential query and fragment from the uri.
+    // Strip out potential query and fragment from the uri.
     $uri = strtok(strtok($uri, "?"), "#");
     // Remove the schema, if any. Otherwise, remove the forwarding "/".
     if (strpos($uri, 'entity:') !== FALSE) {
-      list(, $uri) = explode(':', $uri);
+      $uri_parts = explode(':', $uri);
+      $uri = $uri_parts[1] ?? $uri;
     }
     else {
       $uri = trim($uri, '/');
@@ -35,9 +36,10 @@ public static function getEntityFromUri($uri) {
 
     if ($uri) {
       $parts = explode('/', $uri, 2);
-      if (count($parts) == 2 && ($entity_type = $parts[0]) && ($entity_id = $parts[1])) {
+      if (count($parts) === 2) {
+        [$entity_type, $entity_id] = $parts;
         $entity_manager = \Drupal::entityTypeManager();
-        if ($entity_manager->getDefinition($entity_type, FALSE)) {
+        if ($entity_manager->hasDefinition($entity_type)) {
           if ($entity = $entity_manager->getStorage($entity_type)->load($entity_id)) {
             return \Drupal::service('entity.repository')->getTranslationFromContext($entity);
           }
@@ -110,17 +112,16 @@ public static function uriFromUserInput($input) {
       ->getViaScheme('public')
       ->getDirectoryPath();
 
-    $protocol_matches = [];
-    preg_match('/^([a-z]*?):/', $input, $protocol_matches);
     if (!empty($public_files_dir) && strpos($input, "/$public_files_dir") === 0) {
       return "base:$input";
     }
-    elseif ((count($protocol_matches) > 1 && in_array($protocol_matches[1], UrlHelper::getAllowedProtocols())) || $is_nolink) {
+    $scheme = parse_url($input, PHP_URL_SCHEME);
+    // Check if the input already contains a scheme.
+    if (!empty($scheme)) {
       return $input;
     }
-    else {
-      return "internal:$input";
-    }
+
+    return "internal:$input";
   }
 
   /**
@@ -149,17 +150,18 @@ public static function getEntityFromUserInput($input) {
     try {
       $route_name = Url::fromUri($input)->getRouteName();
       $params = array_filter(Url::fromUri($input)->getRouteParameters());
-      $possibly_an_entity_type = key($params);
-      // Return only the entity, if this is a canonical route.
-      if ($route_name === 'entity.' . $possibly_an_entity_type . '.canonical') {
-        $entity = \Drupal::entityTypeManager()
-          ->getStorage($possibly_an_entity_type)
-          ->load($params[$possibly_an_entity_type]);
-        if (!($entity instanceof EntityInterface)) {
-          return NULL;
+      foreach ($params as $possibly_an_entity_type => $possibly_an_entity_id) {
+        // Return only the entity, if this is a canonical route.
+        if ($route_name === 'entity.' . $possibly_an_entity_type . '.canonical') {
+          $entity = \Drupal::entityTypeManager()
+            ->getStorage($possibly_an_entity_type)
+            ->load($possibly_an_entity_id);
+          if (!($entity instanceof EntityInterface)) {
+            return NULL;
+          }
+          return \Drupal::service('entity.repository')
+            ->getTranslationFromContext($entity);
         }
-        return \Drupal::service('entity.repository')
-          ->getTranslationFromContext($entity);
       }
     }
     catch (\Exception $e) {
@@ -185,8 +187,8 @@ public static function getPathByAlias($input) {
     /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */
     $language_manager = \Drupal::service('language_manager');
 
+    $input_path = parse_url($input, PHP_URL_PATH);
     foreach ($language_manager->getLanguages() as $language) {
-      $input_path = parse_url($input, PHP_URL_PATH);
       if ($prefix = $config->get('url.prefixes.' . $language->getId())) {
         // Strip the language prefix.
         $input_path = preg_replace("/^\/$prefix\//", '/', $input_path);
diff --git a/web/modules/linkit/tests/linkit_test/linkit_test.info.yml b/web/modules/linkit/tests/linkit_test/linkit_test.info.yml
index 5a732b728f10478348207d44c2bee01823d71746..93c3d27ce62cbbb7b5f70ae8da450f08c36610cd 100644
--- a/web/modules/linkit/tests/linkit_test/linkit_test.info.yml
+++ b/web/modules/linkit/tests/linkit_test/linkit_test.info.yml
@@ -2,13 +2,12 @@ name: 'Linkit test module'
 description: 'Support module for Linkit testing.'
 package: Testing
 type: module
-core_version_requirement: ^8.8 || ^9
 dependencies:
   - linkit:linkit
   - drupal:field
   - drupal:text
 
-# Information added by Drupal.org packaging script on 2021-09-28
-version: '8.x-5.0-beta13'
+# Information added by Drupal.org packaging script on 2023-07-07
+version: '6.0.0'
 project: 'linkit'
-datestamp: 1632848793
+datestamp: 1688748027
diff --git a/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php b/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php
index e931e85ee8bfa93f31ee8cd8bc34cada8e86227f..0635e64e78f6088a7076d354431a35b4735362c6 100644
--- a/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php
+++ b/web/modules/linkit/tests/src/Functional/Controller/LinkitControllerTest.php
@@ -24,7 +24,7 @@ class LinkitControllerTest extends LinkitBrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->linkitProfile = $this->createProfile();
diff --git a/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php b/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php
index bc61b6c87080d60ff61f2b735f6e2666dcf1bff4..7a669fe17d343bae338c55da80467026ebd67d1e 100644
--- a/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php
+++ b/web/modules/linkit/tests/src/Functional/LinkitBrowserTestBase.php
@@ -14,7 +14,7 @@ abstract class LinkitBrowserTestBase extends BrowserTestBase {
    *
    * @var array
    */
-  public static $modules = ['linkit', 'linkit_test', 'block'];
+  protected static $modules = ['linkit', 'linkit_test', 'block'];
 
   /**
    * {@inheritdoc}
@@ -38,7 +38,7 @@ abstract class LinkitBrowserTestBase extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->placeBlock('page_title_block');
diff --git a/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php b/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php
deleted file mode 100644
index 2d44842e3f83ce4f2e5498fe3df2f6431944748d..0000000000000000000000000000000000000000
--- a/web/modules/linkit/tests/src/Functional/LinkitUpdateTest.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-namespace Drupal\Tests\linkit\Functional;
-
-use Drupal\filter\Entity\FilterFormat;
-use Drupal\FunctionalTests\Update\UpdatePathTestBase;
-
-/**
- * Tests Linkit upgrade paths.
- *
- * @group Update
- * @group legacy
- */
-class LinkitUpdateTest extends UpdatePathTestBase {
-
-  /**
-   * The config factory service.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $defaultTheme = 'stark';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-    $this->configFactory = $this->container->get('config.factory');
-  }
-
-  /**
-   * Set database dump files to be used.
-   */
-  protected function setDatabaseDumpFiles() {
-    $this->databaseDumpFiles = [
-      __DIR__ . '/../../../tests/fixtures/update/drupal-8.linkit-enabled.standard.php.gz',
-      __DIR__ . '/../../../tests/fixtures/update/linkit-additions.php',
-    ];
-  }
-
-  /**
-   * Tests linkit_update_X.
-   */
-  public function testLinkitUpdate8500() {
-    $editor = $this->configFactory->get('editor.editor.format_1');
-    $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.');
-    $format_1_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile');
-
-    $editor = $this->configFactory->get('editor.editor.format_2');
-    $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.');
-    $format_2_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile');
-
-    $editor = $this->configFactory->get('editor.editor.format_3');
-    $this->assertNotEmpty($editor->get('settings.plugins.linkit'), 'We got old linkit settings in the editor configuration.');
-    $format_3_linkit_profile = $editor->get('settings.plugins.linkit.linkit_profile');
-
-    $test_profile = $this->configFactory->get('linkit.linkit_profile.test_profile');
-    $this->assertNotNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.result_description'), 'Profile have result_description');
-    $this->assertNotNull($test_profile->get('third_party_settings.imce.use'), 'Profile have imce use');
-    $this->assertNotNull($test_profile->get('third_party_settings.imce.scheme'), 'Profile have imce scheme');
-
-    $this->runUpdates();
-
-    $test_profile = $this->configFactory->get('linkit.linkit_profile.test_profile');
-    $this->assertEquals(NULL, $test_profile->get('attributes'), 'Attributes are deleted from the profile.');
-    $this->assertEquals('canonical', $test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.substitution_type'), 'Content matcher has a substitution type of canonical.');
-    $this->assertEquals('file', $test_profile->get('matchers.b8d6d672-6377-493f-b492-3cc69511cf17.settings.substitution_type'), 'File matcher has a substitution type of file.');
-    $this->assertNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.result_description'), 'Profile does not have result_description');
-    $this->assertNotNull($test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.metadata'), 'Profile have metadata');
-    $this->assertNull($test_profile->get('third_party_settings.imce.use'), 'Profile does not have imce use');
-    $this->assertNull($test_profile->get('third_party_settings.imce.scheme'), 'Profile does not have imce scheme');
-
-    $editor = $this->configFactory->get('editor.editor.format_1');
-    $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.');
-    $this->assertEquals($editor->get('settings.toolbar.rows.0.1.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.');
-    $this->assertNotEquals($editor->get('settings.toolbar.rows.0.1.items.1'), 'Linkit', 'Linkit plugin is removed from the toolbar.');
-    $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.');
-    $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_1_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.');
-
-    $editor = $this->configFactory->get('editor.editor.format_2');
-    $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.');
-    $this->assertEquals($editor->get('settings.toolbar.rows.0.1.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.');
-    $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.');
-    $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_2_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.');
-
-    $editor = $this->configFactory->get('editor.editor.format_3');
-    $this->assertNull($editor->get('settings.plugins.linkit'), 'Old linkit settings in the editor configuration is removed.');
-    $this->assertEquals($editor->get('settings.toolbar.rows.0.0.items.0'), 'DrupalLink', 'Drupal link plugin is in the toolbar.');
-    $this->assertNotEmpty($editor->get('settings.plugins.drupallink.linkit_enabled'), 'Drupal link plugin has linkit enabled.');
-    $this->assertEquals($editor->get('settings.plugins.drupallink.linkit_profile'), $format_3_linkit_profile, 'Drupal link plugin uses the same profile as the old linkit plugin.');
-
-    $format = $this->configFactory->get('filter.format.format_1');
-    $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.');
-    $this->assertTrue($format->get('filters.linkit.weight') < $format->get('filters.filter_html.weight'), 'Linkit filter is running before filter_html.');
-
-    $format = $this->configFactory->get('filter.format.format_2');
-    $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.');
-
-    $format = $this->configFactory->get('filter.format.format_3');
-    $this->assertNotNull($format->get('filters.linkit'), 'Linkit filter is enabled.');
-
-    $htmlRestrictions = FilterFormat::load('format_1')->getHtmlRestrictions();
-    $this->assertArrayHasKey("data-entity-type", $htmlRestrictions['allowed']['a']);
-    $this->assertArrayHasKey("data-entity-uuid", $htmlRestrictions['allowed']['a']);
-    $this->assertArrayHasKey("data-entity-substitution", $htmlRestrictions['allowed']['a']);
-
-    $htmlRestrictions = FilterFormat::load('format_3')->getHtmlRestrictions();
-    $this->assertArrayHasKey("data-entity-type", $htmlRestrictions['allowed']['a']);
-    $this->assertArrayHasKey("data-entity-uuid", $htmlRestrictions['allowed']['a']);
-  }
-
-}
diff --git a/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php b/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php
index 2db6653c28398d551170a4988b4f957b29cd451a..07337440c8adec76a4f8ff27dff4711fbe6973a5 100644
--- a/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php
+++ b/web/modules/linkit/tests/src/Functional/MatcherAdminTest.php
@@ -34,7 +34,7 @@ class MatcherAdminTest extends LinkitBrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
     $this->manager = $this->container->get('plugin.manager.linkit.matcher');
 
diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php
index b5a591072c886fe94055a6e26e368bd6630aa3bd..cbcb9f14e10a07aff33e828af16fe415d3a6f151 100644
--- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php
+++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkFieldTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\linkit\FunctionalJavascript;
 
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTestMul;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
@@ -21,7 +23,7 @@ class LinkFieldTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = [
+  protected static $modules = [
     'node',
     'language',
     'field_ui',
@@ -33,7 +35,7 @@ class LinkFieldTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  protected $defaultTheme = 'classy';
+  protected $defaultTheme = 'stark';
 
   /**
    * A linkit profile.
@@ -45,7 +47,7 @@ class LinkFieldTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entityDisplayRepository */
@@ -87,6 +89,10 @@ protected function setUp() {
     $entityDisplayRepository->getViewDisplay('node', 'page', 'default')
       ->setComponent('field_test_link', [
         'type' => 'linkit',
+        'settings' => [
+          'rel' => 'nofollow',
+          'target' => '_blank',
+        ],
       ])
       ->save();
 
@@ -128,7 +134,7 @@ public function testLinkFieldWidgetAndFormatter() {
     $autocomplete_results_wrapper = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete');
     $this->assertTrue($autocomplete_results_wrapper->isVisible());
     $result_description = $assert_session->elementExists('css', 'li.linkit-result-line .linkit-result-line--description', $autocomplete_results_wrapper);
-    $this->assertEquals('Linkit could not find any suggestions. This URL will be used as is.', $result_description->getText());
+    $this->assertEquals('No content suggestions found. This URL will be used as is.', $result_description->getText());
 
     // Set the widget to use our profile and have autofill for link text
     // enabled and try again.
@@ -165,13 +171,13 @@ public function testLinkFieldWidgetAndFormatter() {
 
     // Check that we are viewing the node, and the formatter displays what we
     // expect.
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals('Foo', $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains("/entity_test_mul/manage/{$entity->id()}", $href_value);
+    $assert_session->linkByHrefExists("/entity_test_mul/manage/{$entity->id()}");
+    $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'href', "/entity_test_mul/manage/{$entity->id()}");
+    $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'rel', "nofollow");
+    $assert_session->elementAttributeContains('xpath', '//article/div/div/div[2]/a', 'target', "_blank");
+    $assert_session->linkExists('Foo');
 
-    // Test internal entity targets with anchors.
+    // Test internal entity targets with anchors and query parameters.
     /** @var \Drupal\Core\Entity\EntityInterface $entity */
     $entity2 = EntityTestMul::create(['name' => 'Anchored Entity']);
     $entity2->save();
@@ -196,7 +202,7 @@ public function testLinkFieldWidgetAndFormatter() {
     $this->assertEquals('Anchored Entity', $title_input->getValue());
 
     // Add an anchor to the URL field.
-    $url_input->setValue($entity2->toUrl()->toString() . '#with-anchor');
+    $url_input->setValue($entity2->toUrl()->toString() . '#with-anchor?search=1');
 
     // Give the node a title and save the page.
     $page->fillField('title[0][value]', 'Host test node 2');
@@ -205,11 +211,13 @@ public function testLinkFieldWidgetAndFormatter() {
 
     // Check that we are viewing the node, and the formatter displays what we
     // expect.
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals('Anchored Entity', $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains("/entity_test_mul/manage/{$entity2->id()}#with-anchor", $href_value);
+    $assert_session->linkByHrefExists("/entity_test_mul/manage/{$entity2->id()}#with-anchor?search=1");
+    $assert_session->linkExists('Anchored Entity');
+
+    // Verify anchor persists when visiting the edit form.
+    $this->drupalGet('node/2/edit');
+    $url_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][uri]"]', $widget_wrapper);
+    $this->assertEquals($entity2->toUrl()->toString() . '#with-anchor?search=1', $url_input->getValue());
 
     // Test external URLs.
     $this->drupalGet('node/add/page');
@@ -222,7 +230,7 @@ public function testLinkFieldWidgetAndFormatter() {
     $autocomplete_results_wrapper = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete');
     $this->assertTrue($autocomplete_results_wrapper->isVisible());
     $result_description = $assert_session->elementExists('css', 'li.linkit-result-line .linkit-result-line--description', $autocomplete_results_wrapper);
-    $this->assertEquals('Linkit could not find any suggestions. This URL will be used as is.', $result_description->getText());
+    $this->assertEquals('No content suggestions found. This URL will be used as is.', $result_description->getText());
     $first_result = $assert_session->elementExists('css', 'ul.linkit-ui-autocomplete li.linkit-result-line span.linkit-result-line--title');
     $first_result->click();
     $assert_session->assertWaitOnAjaxRequest();
@@ -238,11 +246,8 @@ public function testLinkFieldWidgetAndFormatter() {
 
     // Check that we are viewing the node, and the formatter displays what we
     // expect.
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals('This is google', $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains('https://google.com#foobar', $href_value);
+    $assert_session->linkByHrefExists('https://google.com#foobar');
+    $assert_session->linkExists('This is google');
 
     // Test that it is possible to add just the anchor.
     $this->drupalGet('node/add/page');
@@ -256,11 +261,8 @@ public function testLinkFieldWidgetAndFormatter() {
     $page->fillField('title[0][value]', 'Host test node 3.5');
     $page->pressButton('Save');
     $assert_session->pageTextContains('Host test node 3.5 has been created');
-
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $href_value = $link_element->getAttribute('href');
-    $this->assertEquals('#foobar', $href_value);
+    $assert_session->linkByHrefExists('#foobar');
+    $assert_session->linkExists('Just a fragment');
 
     // Test emails.
     $this->drupalGet('node/add/page');
@@ -280,7 +282,7 @@ public function testLinkFieldWidgetAndFormatter() {
     $this->assertEquals('mailto:' . $email, $url_input->getValue());
     // Check that the title was populated automatically.
     $title_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][title]"]', $widget_wrapper);
-    $this->assertEquals($email, $title_input->getValue());
+    $this->assertEquals((string) new FormattableMarkup('E-mail @email', ['@email' => $email]), $title_input->getValue());
 
     // Give the node a title and save the page.
     $page->fillField('title[0][value]', 'Host test node 4');
@@ -289,11 +291,8 @@ public function testLinkFieldWidgetAndFormatter() {
 
     // Check that we are viewing the node, and the formatter displays what we
     // expect.
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals($email, $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains('mailto:' . $email, $href_value);
+    $assert_session->linkByHrefExists('mailto:' . $email);
+    $assert_session->linkExists((string) new FormattableMarkup('E-mail @email', ['@email' => $email]));
 
     // Test internal host.
     $this->drupalGet('node/add/page');
@@ -321,11 +320,8 @@ public function testLinkFieldWidgetAndFormatter() {
 
     // Check that we are viewing the node, and the formatter displays what we
     // expect Should display the relative url without the host.
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals($url, $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains('/node/1', $href_value);
+    $assert_session->linkByHrefExists('/node/1');
+    $assert_session->linkExists($url);
 
     // Test front page.
     $this->drupalGet('node/add/page');
@@ -341,7 +337,7 @@ public function testLinkFieldWidgetAndFormatter() {
 
     $url_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][uri]"]', $widget_wrapper);
 
-    $this->assertEquals('/', $url_input->getValue());
+    $this->assertEquals(Url::fromRoute('<front>')->toString(), $url_input->getValue());
     // Check that the title was populated automatically.
     $title_input = $assert_session->elementExists('css', 'input[name="field_test_link[0][title]"]', $widget_wrapper);
     $this->assertEquals('Front page', $title_input->getValue());
@@ -351,11 +347,8 @@ public function testLinkFieldWidgetAndFormatter() {
     $page->pressButton('Save');
     $assert_session->pageTextContains('Host test node 6 has been created');
 
-    $field_wrapper = $assert_session->elementExists('css', '.field--type-link.field--name-field-test-link');
-    $link_element = $assert_session->elementExists('css', 'a', $field_wrapper);
-    $this->assertEquals('Front page', $link_element->getText());
-    $href_value = $link_element->getAttribute('href');
-    $this->assertContains('/', $href_value);
+    $assert_session->linkByHrefExists(Url::fromRoute('<front>')->toString());
+    $assert_session->linkExists('Front page');
 
     // Test invalid input.
     foreach (['foo:0123456', ':', '123:bar'] as $key => $invalid_string) {
diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php
new file mode 100644
index 0000000000000000000000000000000000000000..0cb9865987fee634df957663ef3ee74228f2be7d
--- /dev/null
+++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php
@@ -0,0 +1,228 @@
+<?php
+
+namespace Drupal\Tests\linkit\FunctionalJavascript;
+
+use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
+use Drupal\editor\Entity\Editor;
+use Drupal\entity_test\Entity\EntityTestMul;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\linkit\MatcherInterface;
+use Drupal\linkit\Tests\ProfileCreationTrait;
+use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * Tests the Linkit extensions to the CKEditor 5 Link plugin.
+ *
+ * @group linkit
+ * @group ckeditor5
+ */
+class LinkitDialogCKEditor5Test extends WebDriverTestBase {
+
+  use ProfileCreationTrait;
+  use CKEditor5TestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'ckeditor5',
+    'filter',
+    'linkit',
+    'entity_test',
+    'language',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * An instance of the "CKEditor" text editor plugin.
+   *
+   * @var \Drupal\ckeditor\Plugin\Editor\CKEditor
+   */
+  protected $ckeditor;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $matcher_manager = $this->container->get('plugin.manager.linkit.matcher');
+    $linkit_profile = $this->createProfile();
+    $plugin = $matcher_manager->createInstance('entity:entity_test_mul');
+    assert($plugin instanceof MatcherInterface);
+    $linkit_profile->addMatcher($plugin->getConfiguration());
+    $linkit_profile->save();
+
+    // Create text format, associate CKEditor 5, validate.
+    FilterFormat::create([
+      'format' => 'test_format',
+      'name' => 'Test format',
+      'filters' => [
+        'filter_html' => [
+          'status' => TRUE,
+          'settings' => [
+            'allowed_html' => '<p> <br> <a href data-entity-type data-entity-uuid data-entity-substitution>',
+          ],
+        ],
+      ],
+    ])->save();
+    Editor::create([
+      'format' => 'test_format',
+      'editor' => 'ckeditor5',
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'link',
+          ],
+        ],
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => $linkit_profile->id(),
+          ],
+        ],
+      ],
+    ])->save();
+    $this->assertSame([], array_map(
+      function (ConstraintViolation $v) {
+        return (string) $v->getMessage();
+      },
+      iterator_to_array(CKEditor5::validatePair(
+        Editor::load('test_format'),
+        FilterFormat::load('test_format')
+      ))
+    ));
+
+    // Create a node type for testing.
+    $this->drupalCreateContentType(['type' => 'page']);
+
+    $account = $this->drupalCreateUser([
+      'create page content',
+      'use text format test_format',
+      'view test entity',
+    ]);
+
+    $this->drupalLogin($account);
+  }
+
+  /**
+   * Test the link dialog.
+   */
+  public function testLinkDialog() {
+    $session = $this->getSession();
+    $assert_session = $this->assertSession();
+    $page = $session->getPage();
+
+    // Adds additional languages.
+    $langcodes = ['sv', 'da', 'fi'];
+    foreach ($langcodes as $langcode) {
+      ConfigurableLanguage::createFromLangcode($langcode)->save();
+    }
+
+    // Create a test entity.
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = EntityTestMul::create(['name' => 'Foo']);
+    $entity->save();
+
+    $this->drupalGet('node/add/page');
+    $this->waitForEditor();
+    $this->pressEditorButton('Link');
+
+    // Find the href field.
+    $balloon = $this->assertVisibleBalloon('.ck-link-form');
+    $autocomplete_field = $balloon->find('css', '.ck-input-text');
+
+    // Make sure all fields are empty.
+    $this->assertEmpty($autocomplete_field->getValue(), 'Autocomplete field is empty.');
+
+    // Make sure the autocomplete result container is hidden.
+    $autocomplete_container = $assert_session->elementExists('css', '.ck-link-form .linkit-ui-autocomplete');
+    $this->assertFalse($autocomplete_container->isVisible());
+
+    // Trigger a keydown event to activate a autocomplete search.
+    $autocomplete_field->setValue('f');
+    $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
+    $this->assertTrue($this->getSession()->wait(5000, "document.querySelectorAll('.linkit-result-line.ui-menu-item').length > 0"));
+
+    // Make sure the autocomplete result container is visible.
+    $this->assertTrue($autocomplete_container->isVisible());
+
+    // Make sure the autocomplete result container is excluded from CKEditor5 CSS resets:
+    $assert_session->elementExists('css', '.ck-link-form .linkit-ui-autocomplete.ck-reset_all-excluded');
+
+    // Find all the autocomplete results.
+    $results = $page->findAll('css', '.linkit-result-line.ui-menu-item');
+    $this->assertCount(1, $results);
+
+    // Find the first result and click it.
+    $results[0]->click();
+
+    // Make sure the linkit field field is populated with the test entity's URL.
+    $expected_url = base_path() . 'entity_test_mul/manage/1';
+    $this->assertSame($expected_url, $autocomplete_field->getValue());
+    $balloon->pressButton('Save');
+    // Assert balloon was closed by pressing its "Save" button.
+    $this->assertFalse($page->find('css', '.ck-balloon-panel')->isVisible());
+
+    // Make sure all attributes are populated.
+    $linkit_link = $assert_session->waitForElementVisible('css', '.ck-content a');
+    $this->assertNotNull($linkit_link);
+    $this->assertSame($expected_url, $linkit_link->getAttribute('href'));
+    $this->assertSame('entity_test_mul', $linkit_link->getAttribute('data-entity-type'));
+    $this->assertSame($entity->uuid(), $linkit_link->getAttribute('data-entity-uuid'));
+    $this->assertSame('canonical', $linkit_link->getAttribute('data-entity-substitution'));
+
+    // Open the edit link dialog by moving selection to the link, verifying the
+    // "Link" button is off before and on after, and then pressing that button.
+    $this->assertFalse($this->getEditorButton('Link')->hasClass('ck-on'));
+    $this->selectTextInsideElement('a');
+    $this->assertTrue($this->getEditorButton('Link')->hasClass('ck-on'));
+    $this->pressEditorButton('Link');
+    $this->assertVisibleBalloon('.ck-link-actions');
+    $edit_button = $this->getBalloonButton('Edit link');
+    $edit_button->click();
+    $link_edit_balloon = $this->assertVisibleBalloon('.ck-link-form');
+    $autocomplete_field = $link_edit_balloon->find('css', '.ck-input-text');
+    $this->assertSame($expected_url, $autocomplete_field->getValue());
+    // Click to trigger the reset of the the autocomplete status.
+    $autocomplete_field->click();
+    // Enter a URL and verify that no link suggestions are found.
+    $autocomplete_field->setValue('http://example.com');
+    $autocomplete_field->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->waitForElementVisible('css', '.linkit-result-line.ui-menu-item');
+    $results = $page->findAll('css', '.linkit-result-line.ui-menu-item');
+    $this->assertCount(1, $results);
+    $this->assertSame('http://example.com', $results[0]->find('css', '.linkit-result-line--title')->getText());
+    $this->assertSame('No content suggestions found. This URL will be used as is.', $results[0]->find('css', '.linkit-result-line--description')->getText());
+    // Decline the autocomplete suggestion.
+    $link_edit_balloon->pressButton('Cancel');
+    // Accept the link as-is.
+    $link_edit_balloon->pressButton('Save');
+    $this->assertTrue($assert_session->waitForElementRemoved('css', '.ck-button-save'));
+    // Assert balloon is still visible, but now it's again the link actions one.
+    $this->assertVisibleBalloon('.ck-link-actions');
+    // Assert balloon can be closed by clicking elsewhere in the editor.
+    $page->find('css', '.ck-editor__editable')->click();
+    $this->assertFalse($page->find('css', '.ck-balloon-panel')->isVisible());
+
+    $changed_link = $assert_session->waitForElementVisible('css', '.ck-content [href="http://example.com"]');
+    $this->assertNotNull($changed_link);
+    foreach ([
+      'data-entity-type',
+      'data-entity-uuid',
+      'data-entity-substitution',
+    ] as $attribute_name) {
+      $this->assertFalse($changed_link->hasAttribute($attribute_name), "Link should no longer have $attribute_name");
+    }
+  }
+
+}
diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php
index 57ca481d2ba9db355b094c665c5453521ff65d77..b53cceaea96789f2fd2e28f2a9a631e2cdc8a8eb 100644
--- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php
+++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitDialogTest.php
@@ -27,9 +27,8 @@ class LinkitDialogTest extends WebDriverTestBase {
    *
    * @var array
    */
-  public static $modules = [
+  protected static $modules = [
     'node',
-    'ckeditor',
     'filter',
     'linkit',
     'entity_test',
@@ -58,9 +57,15 @@ class LinkitDialogTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
+    if (!in_array('ckeditor', $this->container->get('extension.list.module')->reset()->getList(), TRUE)) {
+      $this->markTestSkipped('CKEditor 4 module not available to install, skipping test.');
+    }
+    $this->container->get('module_installer')->install(['ckeditor']);
+    $this->container = \Drupal::getContainer();
+
     $matcherManager = $this->container->get('plugin.manager.linkit.matcher');
     /** @var \Drupal\linkit\MatcherInterface $plugin */
 
@@ -160,6 +165,9 @@ public function testLinkDialog() {
     // Wait for the form to load.
     $web_assert->assertWaitOnAjaxRequest();
 
+    // The dialog is not tall enough to allow the autocomplete to be visible.
+    $this->getSession()->executeScript("document.getElementById('drupal-modal').style.height = '200px';");
+
     // Find the href field.
     $href_field = $page->findField('attributes[href]');
 
@@ -196,6 +204,9 @@ public function testLinkDialog() {
     // Find the first result and click it.
     $page->find('xpath', '//li[contains(@class, "linkit-result-line") and contains(@class, "ui-menu-item")][1]')->click();
 
+    // Make sure the linkit field field is populated with the node url.
+    $this->assertEquals($entity->toUrl()->toString(), $href_field->getValue(), 'The href field is populated with the node url.');
+
     // Make sure all other fields are populated.
     $this->assertEqualsWithJs('attributes[data-entity-type]', $entity->getEntityTypeId());
     $this->assertEqualsWithJs('attributes[data-entity-uuid]', $entity->uuid());
diff --git a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php
index 3abe5a4d7014eb17d820194197f7b99a191c6c49..f7f4bd27380e2592d5a890ee581c0db698e85aba 100644
--- a/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php
+++ b/web/modules/linkit/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php
@@ -16,7 +16,7 @@ class LinkitFormatAdminTest extends WebDriverTestBase {
    *
    * @var array
    */
-  public static $modules = ['editor', 'filter', 'linkit'];
+  protected static $modules = ['editor', 'filter', 'linkit'];
 
   /**
    * {@inheritdoc}
@@ -26,7 +26,7 @@ class LinkitFormatAdminTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $account = $this->drupalCreateUser([
@@ -49,15 +49,15 @@ public function testToggleLinkitFilter() {
     $page->findField('filters[filter_html][status]')->check();
 
     $javascript = "(function (){ return jQuery('p.editor-update-message > strong').text(); })()";
-    $this->assertNotContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
+    $this->assertStringNotContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
 
     // Enable the 'Linkit filter' filter.
     $page->findField('filters[linkit][status]')->check();
-    $this->assertContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
+    $this->assertStringContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
 
     // Disable the 'Linkit filter' filter.
     $page->findField('filters[linkit][status]')->uncheck();
-    $this->assertNotContains('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
+    $this->assertStringNotContainsString('<a href hreflang data-entity-substitution data-entity-type data-entity-uuid title>', $session->evaluateScript($javascript));
   }
 
 }
diff --git a/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php b/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php
index 24ca82273e5612f3676df11cb41f4876ea16e0ac..3e77f69e0e6f75e11d3e7ba1af366a9d79c557a1 100644
--- a/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php
+++ b/web/modules/linkit/tests/src/Kernel/AssertLinkitFilterTrait.php
@@ -29,7 +29,7 @@ trait AssertLinkitFilterTrait {
   protected function assertLinkitFilter(EntityInterface $entity, $langcode = LanguageInterface::LANGCODE_SITE_DEFAULT) {
     if ($entity->getEntityTypeId() === "file") {
       /** @var \Drupal\file\Entity\File $entity */
-      $href = file_create_url($entity->getFileUri());
+      $href = \Drupal::service('file_url_generator')->generateString($entity->getFileUri());
     }
     else {
       $href = $entity->toUrl()->toString();
@@ -38,6 +38,8 @@ protected function assertLinkitFilter(EntityInterface $entity, $langcode = Langu
     $input = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '">Link text</a>';
     $expected = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '" href="' . $href . '">Link text</a>';
     $this->assertSame($expected, $this->process($input, $langcode)->getProcessedText());
+    $canonical_url_aka_not_path_alias = '/entity_test_mul/manage/1';
+    $this->assertStringNotContainsString($canonical_url_aka_not_path_alias, $this->process($input, $langcode)->getProcessedText());
   }
 
   /**
@@ -51,7 +53,7 @@ protected function assertLinkitFilter(EntityInterface $entity, $langcode = Langu
   protected function assertLinkitFilterWithTitle(EntityInterface $entity, $langcode = LanguageInterface::LANGCODE_SITE_DEFAULT) {
     if ($entity->getEntityTypeId() === "file") {
       /** @var \Drupal\file\Entity\File $entity */
-      $href = file_create_url($entity->getFileUri());
+      $href = \Drupal::service('file_url_generator')->generateString($entity->getFileUri());
     }
     else {
       $href = $entity->toUrl()->toString();
diff --git a/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8babece4ce356d1aa37fb020fb55e4b9bb2dbccc
--- /dev/null
+++ b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathCompletenessTest.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\linkit\Kernel\CKEditor4To5Upgrade;
+
+use Drupal\Tests\ckeditor5\Kernel\CKEditor4to5UpgradeCompletenessTest as Real;
+use Drupal\KernelTests\KernelTestBase;
+
+if (class_exists(Real::class)) {
+  class CKEditor4to5UpgradeCompletenessTest extends Real { }
+} else {
+  class CKEditor4to5UpgradeCompletenessTest extends KernelTestBase {
+    public function testImpossible() {
+      $this->markTestSkipped();
+    }
+  }
+}
+
+/**
+ * @covers \Drupal\linkit\Plugin\CKEditor4To5Upgrade\Linkit
+ * @group linkit
+ * @group ckeditor5
+ * @internal
+ * @requires module ckeditor5
+ */
+class UpgradePathCompletenessTest extends CKEditor4to5UpgradeCompletenessTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['linkit'];
+
+}
diff --git a/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..eff4f83c8ef4e63a28f53e08718e935860fa2727
--- /dev/null
+++ b/web/modules/linkit/tests/src/Kernel/CKEditor4To5Upgrade/UpgradePathTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\linkit\Kernel\CKEditor4To5Upgrade;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\ckeditor5\Kernel\SmartDefaultSettingsTest;
+
+/**
+ * @covers \Drupal\linkit\Plugin\CKEditor4To5Upgrade\Linkit
+ * @group linkit
+ * @group ckeditor5
+ * @requires module ckeditor5
+ * @internal
+ */
+class UpgradePathTest extends SmartDefaultSettingsTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'linkit',
+    // Because modules/linkit/config/optional/linkit.linkit_profile.default.yml
+    // will only then get installed.
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installConfig(['linkit']);
+
+    $filter_config = [
+      'filter_html' => [
+        'status' => 1,
+        'settings' => [
+          'allowed_html' => '<p> <br> <strong> <a href>',
+        ],
+      ],
+    ];
+    FilterFormat::create([
+      'format' => 'linkit_disabled',
+      'name' => 'Linkit disabled',
+      'filters' => $filter_config,
+    ])->setSyncing(TRUE)->save();
+    FilterFormat::create([
+      'format' => 'linkit_enabled_misconfigured_format',
+      'name' => 'Linkit enabled on a misconfigured format',
+      'filters' => $filter_config,
+    ])->setSyncing(TRUE)->save();
+    FilterFormat::create([
+      'format' => 'linkit_enabled',
+      'name' => 'Linkit enabled on a well-configured format',
+      'filters' => [
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<p> <br> <strong> <a href data-entity-type data-entity-uuid data-entity-substitution>',
+          ],
+        ],
+      ],
+    ])->setSyncing(TRUE)->save();
+
+    $generate_editor_settings = function (array $linkit_cke4_settings) {
+      return [
+        'toolbar' => [
+          'rows' => [
+            0 => [
+              [
+                'name' => 'Basic Formatting',
+                'items' => [
+                  'Bold',
+                  'Format',
+                  'DrupalLink'
+                ],
+              ],
+            ],
+          ],
+        ],
+        'plugins' => [
+          'drupallink' => $linkit_cke4_settings,
+        ],
+      ];
+    };
+
+    Editor::create([
+      'format' => 'linkit_disabled',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings([
+        'linkit_enabled' => FALSE,
+        'linkit_profile' => '',
+      ]),
+    ])->setSyncing(TRUE)->save();
+    Editor::create([
+      'format' => 'linkit_enabled_misconfigured_format',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings([
+        'linkit_enabled' => TRUE,
+        'linkit_profile' => 'default',
+      ]),
+    ])->setSyncing(TRUE)->save();
+    Editor::create([
+      'format' => 'linkit_enabled',
+      'editor' => 'ckeditor',
+      'settings' => $generate_editor_settings([
+        'linkit_enabled' => TRUE,
+        'linkit_profile' => 'default',
+      ]),
+    ])->setSyncing(TRUE)->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function provider() {
+    $expected_ckeditor5_toolbar = [
+      'items' => [
+        'bold',
+        'link',
+      ],
+    ];
+
+    yield "linkit disabled" => [
+      'format_id' => 'linkit_disabled',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => $expected_ckeditor5_toolbar,
+        'plugins' => [],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [],
+    ];
+
+    yield "linkit enabled on a misconfigured text format" => [
+      'format_id' => 'linkit_enabled_misconfigured_format',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => $expected_ckeditor5_toolbar,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => 'default',
+          ],
+        ],
+      ],
+      'expected_superset' => '<a data-entity-type data-entity-uuid data-entity-substitution>',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [
+        'warning' => [
+          'Updating to CKEditor 5 added support for some previously unsupported tags/attributes. A plugin introduced support for the following:   These attributes: <em class="placeholder"> data-entity-type (for &lt;a&gt;), data-entity-uuid (for &lt;a&gt;), data-entity-substitution (for &lt;a&gt;)</em>; Additional details are available in your logs.',
+        ],
+      ],
+    ];
+
+    yield "linkit enabled on a well-configured text format" => [
+      'format_id' => 'linkit_enabled',
+      'filters_to_drop' => [],
+      'expected_ckeditor5_settings' => [
+        'toolbar' => $expected_ckeditor5_toolbar,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => 'default',
+          ],
+        ],
+      ],
+      'expected_superset' => '',
+      'expected_fundamental_compatibility_violations' => [],
+      'expected_db_logs' => [],
+      'expected_messages' => [],
+    ];
+
+    // Verify that none of the core test cases are broken; especially important
+    // for Linkit since it extends the behavior of Drupal core.
+    foreach (parent::provider() as $label => $case) {
+      yield $label => $case;
+    }
+  }
+
+}
diff --git a/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php b/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php
index cf3832a7e891e087fb24ca44c74e3381ebbeab87..c8c4c5539284052b2a752f1186401c336ef1ed58 100644
--- a/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php
+++ b/web/modules/linkit/tests/src/Kernel/EntityMatcherDeriverTest.php
@@ -12,7 +12,7 @@ class EntityMatcherDeriverTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['block', 'block_content', 'node', 'field'];
+  protected static $modules = ['block', 'block_content', 'node', 'field'];
 
   /**
    * The matcher manager.
@@ -24,7 +24,7 @@ class EntityMatcherDeriverTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->installConfig(['block_content']);
diff --git a/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php b/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php
index b4498e55311ffc7087234c2f461179b23ee4c80f..a5595713ad91791119e8e4877857b6f9ffb37d76 100644
--- a/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php
+++ b/web/modules/linkit/tests/src/Kernel/LinkitAutocompleteTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\linkit\Kernel;
 
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Html;
 use Drupal\entity_test\Entity\EntityTest;
@@ -23,7 +24,7 @@ class LinkitAutocompleteTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['entity_test', 'language'];
+  protected static $modules = ['entity_test', 'language'];
 
   /**
    * The linkit profile.
@@ -49,7 +50,7 @@ class LinkitAutocompleteTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     // Create user 1 who has special permissions.
@@ -109,7 +110,7 @@ public function testAutocompletionEmail() {
 
     $email = 'drupal@example.com';
     $data = $this->getAutocompleteResult($email);
-    $this->assertSame($email, $data[0]['label'], 'Autocomplete returned email suggestion.');
+    $this->assertSame((string) new FormattableMarkup('E-mail @email', ['@email' => $email]), $data[0]['label'], 'Autocomplete returned email suggestion.');
     $this->assertSame('mailto:' . $email, $data[0]['path'], 'Autocomplete returned email suggestion with an mailto href.');
   }
 
diff --git a/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php b/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php
index 42504fcdb6456686f405c5f45d2c641fe59e0667..cd17291d83c15c08339725f594f7cb81c5b9c72b 100644
--- a/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php
+++ b/web/modules/linkit/tests/src/Kernel/LinkitEditorLinkDialogTest.php
@@ -38,16 +38,20 @@ class LinkitEditorLinkDialogTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['editor', 'ckeditor', 'entity_test'];
+  protected static $modules = ['editor', 'entity_test'];
 
   /**
    * Sets up the test.
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
+    if (!in_array('ckeditor', $this->container->get('extension.list.module')->reset()->getList(), TRUE)) {
+      $this->markTestSkipped('CKEditor 4 module not available to install, skipping test.');
+    }
+    $this->enableModules(['ckeditor']);
+
     $this->installEntitySchema('entity_test');
-    $this->installSchema('system', ['key_value_expire']);
 
     // Create a profile.
     $this->linkitProfile = $this->createProfile();
@@ -75,14 +79,11 @@ protected function setUp() {
       'format' => 'filtered_html',
       'editor' => 'ckeditor',
     ]);
-    $this->editor->setSettings([
-      'plugins' => [
-        'drupallink' => [
-          'linkit_enabled' => TRUE,
-          'linkit_profile' => $this->linkitProfile->id(),
-        ],
-      ],
-    ]);
+    $this->editor->save();
+    $settings = $this->editor->getSettings();
+    $settings['plugins']['drupallink']['linkit_enabled'] = TRUE;
+    $settings['plugins']['drupallink']['linkit_profile'] = $this->linkitProfile->id();
+    $this->editor->setSettings($settings);
     $this->editor->save();
   }
 
@@ -128,14 +129,16 @@ public function testAdd() {
 
     $form_state->setValue(['attributes', 'href'], 'https://example.com/');
     $form_state->setValue('href_dirty_check', '');
-    $form_state->setValue(['attributes', 'data-entity-type'], $entity->getEntityTypeId());
+    $form_state->setValue(['attributes', 'data-entity-type'], $this->randomString());
     $form_state->setValue(['attributes', 'data-entity-uuid'], $this->randomString());
     $form_state->setValue(['attributes', 'data-entity-substitution'], $this->randomString());
     $form_builder->submitForm($form_object, $form_state);
     $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-type']));
     $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-uuid']));
-    $this->assertEmpty($form_state->getValue(['attributes', 'data-entity-substitution']));
-
+    $this->assertEmpty($form_state->getValue([
+      'attributes',
+      'data-entity-substitution',
+    ]));
     $entity_url = $entity->toUrl('canonical', ['path_processing' => FALSE])->toString();
     $form_state->setValue(['attributes', 'href'], $entity_url);
     $form_state->setValue('href_dirty_check', $entity_url);
@@ -144,9 +147,18 @@ public function testAdd() {
     $form_state->setValue(['attributes', 'data-entity-substitution'], SubstitutionManagerInterface::DEFAULT_SUBSTITUTION);
     $form_builder->submitForm($form_object, $form_state);
 
-    $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.');
-    $this->assertEquals($entity->uuid(), $form_state->getValue(['attributes', 'data-entity-uuid']), 'Attribute "data-entity-uuid" exists and has the correct value.');
-    $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue(['attributes', 'data-entity-substitution']), 'Attribute "data-entity-substitution" exists and has the correct value.');
+    $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue([
+      'attributes',
+      'data-entity-type',
+    ]), 'Attribute "data-entity-type" exists and has the correct value.');
+    $this->assertEquals($entity->uuid(), $form_state->getValue([
+      'attributes',
+      'data-entity-uuid',
+    ]), 'Attribute "data-entity-uuid" exists and has the correct value.');
+    $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue([
+      'attributes',
+      'data-entity-substitution',
+    ]), 'Attribute "data-entity-substitution" exists and has the correct value.');
   }
 
   /**
@@ -194,9 +206,18 @@ public function testEditWithDataAttributes() {
 
     $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.');
     $this->assertEquals($entity_url, $form['attributes']['href']['#default_value'], 'The href field has the url as default value.');
-    $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.');
-    $this->assertEquals($entity->uuid(), $form_state->getValue(['attributes', 'data-entity-uuid']), 'Attribute "data-entity-uuid" exists and has the correct value.');
-    $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue(['attributes', 'data-entity-substitution']), 'Attribute "data-entity-substitution" exists and has the correct value.');
+    $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue([
+      'attributes',
+      'data-entity-type',
+    ]), 'Attribute "data-entity-type" exists and has the correct value.');
+    $this->assertEquals($entity->uuid(), $form_state->getValue([
+      'attributes',
+      'data-entity-uuid',
+    ]), 'Attribute "data-entity-uuid" exists and has the correct value.');
+    $this->assertEquals(SubstitutionManagerInterface::DEFAULT_SUBSTITUTION, $form_state->getValue([
+      'attributes',
+      'data-entity-substitution',
+    ]), 'Attribute "data-entity-substitution" exists and has the correct value.');
   }
 
   /**
diff --git a/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php b/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php
index 94897b37472e4d330b1d3f80e756f6238bc8187b..149e2c90ca6ea3f46c6aaba3f5c91a0b75348147 100644
--- a/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php
+++ b/web/modules/linkit/tests/src/Kernel/LinkitFilterEntityTest.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\Tests\linkit\Kernel;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\entity_test\Entity\EntityTestMul;
 use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
 use Drupal\filter\FilterPluginCollection;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\Traits\Core\PathAliasTestTrait;
@@ -26,7 +28,7 @@ class LinkitFilterEntityTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = [
+  protected static $modules = [
     'filter',
     'entity_test',
     'path',
@@ -38,7 +40,7 @@ class LinkitFilterEntityTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->installEntitySchema('entity_test');
@@ -56,6 +58,18 @@ protected function setUp() {
     $this->filter = $bag->get('linkit');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+
+    // Undo what the parent did, to allow testing path aliases in kernel tests.
+    $container->getDefinition('path_alias.path_processor')
+      ->addTag('path_processor_inbound')
+      ->addTag('path_processor_outbound');
+  }
+
   /**
    * Tests the linkit filter for entities with different access.
    */
@@ -122,7 +136,7 @@ public function testFilterFileEntity() {
       'filename' => 'druplicon.txt',
       'uri' => 'public://druplicon.txt',
       'filemime' => 'text/plain',
-      'status' => FILE_STATUS_PERMANENT,
+      'status' => FileInterface::STATUS_PERMANENT,
     ]);
     $file->save();
 
@@ -161,8 +175,8 @@ public function testQueryAndFragments() {
 
     // Make sure original query and fragment are preserved.
     $input = '<a data-entity-type="' . $entity->getEntityTypeId() . '" data-entity-uuid="' . $entity->uuid() . '" href="unimportant/1234?query=string#fragment">Link text</a>';
-    $this->assertContains('?query=string', $this->process($input)->getProcessedText());
-    $this->assertContains('#fragment', $this->process($input)->getProcessedText());
+    $this->assertStringContainsString('?query=string', $this->process($input)->getProcessedText());
+    $this->assertStringContainsString('#fragment', $this->process($input)->getProcessedText());
   }
 
 }
diff --git a/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php b/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php
index d04a2bc1cba504f41e4654a1910a2809f5e67dd1..b54e917eead93dfd4600e5f52e8b69a576acbbc8 100644
--- a/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php
+++ b/web/modules/linkit/tests/src/Kernel/LinkitKernelTestBase.php
@@ -16,7 +16,7 @@ abstract class LinkitKernelTestBase extends KernelTestBase {
    *
    * @var array
    */
-  public static $modules = [
+  protected static $modules = [
     'system',
     'user',
     'filter',
@@ -28,7 +28,7 @@ abstract class LinkitKernelTestBase extends KernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
     $this->installSchema('system', 'sequences');
     $this->installEntitySchema('user');
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php
index 7d805033712a6bf48211a5f8728d195b9c31fb91..37db55f55587803309cd2117639ef956c7b2bf04 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/ContactFormMatcherTest.php
@@ -17,7 +17,7 @@ class ContactFormMatcherTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['contact'];
+  protected static $modules = ['contact'];
 
   /**
    * The matcher manager.
@@ -29,13 +29,13 @@ class ContactFormMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     // Create user 1 who has special permissions.
     $this->createUser();
 
-    \Drupal::currentUser()->setAccount($this->createUser([], ['access site-wide contact form', 'view test entity translations']));
+    \Drupal::currentUser()->setAccount($this->createUser([], ['access site-wide contact form']));
 
     $this->manager = $this->container->get('plugin.manager.linkit.matcher');
 
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php
index 4999aae1ed639fa3f586ea402e0c0d900e0f5e47..1c90f1c1a8515b867c654b1586ec51c5568629de 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/FileMatcherTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\linkit\Kernel\Matchers;
 
 use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
 use Drupal\Tests\linkit\Kernel\LinkitKernelTestBase;
 
 /**
@@ -17,7 +18,7 @@ class FileMatcherTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['file_test', 'file'];
+  protected static $modules = ['file_test', 'file'];
 
   /**
    * The matcher manager.
@@ -29,11 +30,10 @@ class FileMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->installEntitySchema('file');
-    $this->installSchema('system', ['key_value_expire']);
     $this->installSchema('file', ['file_usage']);
 
     $this->manager = $this->container->get('plugin.manager.linkit.matcher');
@@ -45,7 +45,7 @@ protected function setUp() {
         'filename' => 'image-test.' . $ext,
         'uri' => 'public://image-test.' . $ext,
         'filemime' => 'text/plain',
-        'status' => FILE_STATUS_PERMANENT,
+        'status' => FileInterface::STATUS_PERMANENT,
       ]);
       $file->save();
     }
@@ -105,8 +105,8 @@ public function testTermMatcherWidthMetadataTokens() {
     $suggestions = $suggestionCollection->getSuggestions();
 
     foreach ($suggestions as $suggestion) {
-      $this->assertNotContains('[file:fid]', $suggestion->getDescription(), 'Raw token "[file:fid]" is not present in the description');
-      $this->assertNotContains('[file:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[file:field_with_no_value]" is not present in the description');
+      $this->assertStringNotContainsString('[file:fid]', $suggestion->getDescription(), 'Raw token "[file:fid]" is not present in the description');
+      $this->assertStringNotContainsString('[file:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[file:field_with_no_value]" is not present in the description');
     }
   }
 
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php
index 1302bdfa99bab71acbf0da1ffb2484f578355921..64013a6e8ac233b01730f31d41f3c083a5c07ca8 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/MediaMatcherTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\linkit\Kernel\Matchers;
 
 use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
 use Drupal\media\Entity\Media;
 use Drupal\media\Entity\MediaType;
 use Drupal\Tests\linkit\Kernel\LinkitKernelTestBase;
@@ -19,7 +20,7 @@ class MediaMatcherTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['file_test', 'file', 'media', 'image', 'field'];
+  protected static $modules = ['file_test', 'file', 'media', 'image', 'field'];
 
   /**
    * The matcher manager.
@@ -31,13 +32,12 @@ class MediaMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->installEntitySchema('file');
     $this->installEntitySchema('media');
     $this->installConfig(['media']);
-    $this->installSchema('system', ['key_value_expire']);
     $this->installSchema('file', ['file_usage']);
 
     $this->manager = $this->container->get('plugin.manager.linkit.matcher');
@@ -64,7 +64,7 @@ protected function setUp() {
         'filename' => 'image-test.' . $ext,
         'uri' => 'public://image-test.' . $ext,
         'filemime' => 'text/plain',
-        'status' => FILE_STATUS_PERMANENT,
+        'status' => FileInterface::STATUS_PERMANENT,
       ]);
       $file->save();
 
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php
index 0ab55506edb60546da7e1672e204e020c322fa80..4ee67a880b3eb0033dfdaa0e550ba1d05be4f1fb 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/NodeMatcherTest.php
@@ -18,7 +18,7 @@ class NodeMatcherTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['field', 'node', 'content_moderation', 'workflows'];
+  protected static $modules = ['field', 'node', 'content_moderation', 'workflows'];
 
   /**
    * The matcher manager.
@@ -30,7 +30,7 @@ class NodeMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     $this->installEntitySchema('node');
@@ -175,8 +175,8 @@ public function testNodeMatcherWidthMetadataTokens() {
     $suggestions = $suggestionCollection->getSuggestions();
 
     foreach ($suggestions as $suggestion) {
-      $this->assertNotContains('[node:nid]', $suggestion->getDescription(), 'Raw token "[node:nid]" is not present in the description');
-      $this->assertNotContains('[node:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[node:field_with_no_value]" is not present in the description');
+      $this->assertStringNotContainsString('[node:nid]', $suggestion->getDescription(), 'Raw token "[node:nid]" is not present in the description');
+      $this->assertStringNotContainsString('[node:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[node:field_with_no_value]" is not present in the description');
     }
   }
 
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php
index 4e04d0fde9bca5eef604cb2762c009546bbd4550..98d70206d0cec3a62201502f4686c0b520c81340 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/TermMatcherTest.php
@@ -20,7 +20,7 @@ class TermMatcherTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['taxonomy'];
+  protected static $modules = ['taxonomy'];
 
   /**
    * The matcher manager.
@@ -32,7 +32,7 @@ class TermMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     // Create user 1 who has special permissions.
@@ -98,8 +98,8 @@ public function testTermMatcherWidthMetadataTokens() {
     $suggestions = $suggestionCollection->getSuggestions();
 
     foreach ($suggestions as $suggestion) {
-      $this->assertNotContains('[term:nid]', $suggestion->getDescription(), 'Raw token "[term:nid]" is not present in the description');
-      $this->assertNotContains('[term:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[term:field_with_no_value]" is not present in the description');
+      $this->assertStringNotContainsString('[term:nid]', $suggestion->getDescription(), 'Raw token "[term:nid]" is not present in the description');
+      $this->assertStringNotContainsString('[term:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[term:field_with_no_value]" is not present in the description');
     }
   }
 
diff --git a/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php b/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php
index a9d8df5b348122189ba724db46803ba04ba54ade..d2ef802604553d95b8289fe4a668251d4c496b2d 100644
--- a/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php
+++ b/web/modules/linkit/tests/src/Kernel/Matchers/UserMatcherTest.php
@@ -22,7 +22,7 @@ class UserMatcherTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp(): void {
     parent::setUp();
 
     // Create user 1 who has special permissions.
@@ -130,8 +130,8 @@ public function testTermMatcherWidthMetadataTokens() {
     $suggestions = $suggestionCollection->getSuggestions();
 
     foreach ($suggestions as $suggestion) {
-      $this->assertNotContains('[user:uid]', $suggestion->getDescription(), 'Raw token "[user:nid]" is not present in the description');
-      $this->assertNotContains('[user:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[user:field_with_no_value]" is not present in the description');
+      $this->assertStringNotContainsString('[user:uid]', $suggestion->getDescription(), 'Raw token "[user:nid]" is not present in the description');
+      $this->assertStringNotContainsString('[user:field_with_no_value]', $suggestion->getDescription(), 'Raw token "[user:field_with_no_value]" is not present in the description');
     }
   }
 
diff --git a/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php b/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php
index 0bc6202e6e3bb8549c8fa3f422b997e90273c0c5..32f9ed647c8c97b39e8f5fb7c26017b2b08a4a56 100644
--- a/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php
+++ b/web/modules/linkit/tests/src/Kernel/SubstitutionPluginTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Site\Settings;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
 use Drupal\linkit\Plugin\Linkit\Substitution\Canonical as CanonicalSubstitutionPlugin;
 use Drupal\linkit\Plugin\Linkit\Substitution\File as FileSubstitutionPlugin;
 use Drupal\linkit\Plugin\Linkit\Substitution\Media as MediaSubstitutionPlugin;
@@ -38,7 +39,7 @@ class SubstitutionPluginTest extends LinkitKernelTestBase {
    *
    * @var array
    */
-  public static $modules = [
+  protected static $modules = [
     'file',
     'entity_test',
     'media',
@@ -50,7 +51,7 @@ class SubstitutionPluginTest extends LinkitKernelTestBase {
   /**
    * {@inheritdoc}
    */
-  public function setUp() {
+  public function setUp(): void {
     parent::setUp();
     $this->substitutionManager = $this->container->get('plugin.manager.linkit.substitution');
     $this->entityTypeManager = $this->container->get('entity_type.manager');
@@ -106,10 +107,10 @@ public function testFileSubstitutions() {
       'filename' => 'druplicon.txt',
       'uri' => 'public://druplicon.txt',
       'filemime' => 'text/plain',
-      'status' => FILE_STATUS_PERMANENT,
+      'status' => FileInterface::STATUS_PERMANENT,
     ]);
     $file->save();
-    $this->assertEquals($GLOBALS['base_url'] . '/' . $this->siteDirectory . '/files/druplicon.txt', $fileSubstitution->getUrl($file)->getGeneratedUrl());
+    $this->assertEquals('/' . $this->siteDirectory . '/files/druplicon.txt', $fileSubstitution->getUrl($file)->getGeneratedUrl());
 
     $entity_type = $this->entityTypeManager->getDefinition('file');
     $this->assertTrue(FileSubstitutionPlugin::isApplicable($entity_type), 'The entity type File is applicable the file substitution.');
@@ -158,7 +159,7 @@ public function testMediaSubstitution() {
       'filename' => 'druplicon.txt',
       'uri' => 'public://druplicon.txt',
       'filemime' => 'text/plain',
-      'status' => FILE_STATUS_PERMANENT,
+      'status' => FileInterface::STATUS_PERMANENT,
     ]);
     $file->save();
 
@@ -169,7 +170,7 @@ public function testMediaSubstitution() {
     $media->save();
 
     $media_substitution = $this->substitutionManager->createInstance('media');
-    $expected = $GLOBALS['base_url'] . '/' . $this->siteDirectory . '/files/druplicon.txt';
+    $expected = '/' . $this->siteDirectory . '/files/druplicon.txt';
     $this->assertEquals($expected, $media_substitution->getUrl($media)->getGeneratedUrl());
 
     // Ensure the url is identical when media entities have a standalone URL
diff --git a/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php b/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d7fa6122306ea259ca0bea18a056daf6e1ed86b
--- /dev/null
+++ b/web/modules/linkit/tests/src/Kernel/ValidatorsTest.php
@@ -0,0 +1,136 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\linkit\Kernel;
+
+use Drupal\Tests\ckeditor5\Kernel\ValidatorsTest as CKEditor5CoreValidatorsTest;
+
+/**
+ * @covers \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::validChoices
+ * @covers \Drupal\linkit\Plugin\CKEditor5Plugin\Linkit::requireProfileIfEnabled
+ * @covers linkit.schema.yml
+ *
+ * @group linkit
+ */
+class ValidatorsTest extends CKEditor5CoreValidatorsTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'linkit',
+    // @see config/optional/linkit.linkit_profile.default.yml
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // @see config/optional/linkit.linkit_profile.default.yml
+    $this->installConfig(['linkit']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function provider(): array {
+    $linkit_test_cases_toolbar_settings = ['items' => ['link']];
+
+    $data = [];
+    $data['VALID: installing the linkit module without configuring the existing text editors'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [],
+      ],
+      'violations' => [],
+    ];
+    $data['INVALID: linkit — invalid manually created configuration'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => 'no',
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.linkit_extension.linkit_enabled' => 'This value should be of the correct primitive type.',
+      ],
+    ];
+    $data['VALID: linkit off'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => FALSE,
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
+    $data['VALID: linkit off, profile selected'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => 'default',
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
+    $data['INVALID: linkit on, no profile selected'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.linkit_extension.linkit_profile' => 'Linkit is enabled, please select the Linkit profile you wish to use.',
+      ],
+    ];
+    $data['INVALID: linkit on, non-existent profile selected'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => 'nonexistent',
+          ],
+        ],
+      ],
+      'violations' => [
+        'settings.plugins.linkit_extension.linkit_profile' => 'The value you selected is not a valid choice.',
+      ],
+    ];
+    $data['VALID: linkit on, existing profile selected'] = [
+      'settings' => [
+        'toolbar' => $linkit_test_cases_toolbar_settings,
+        'plugins' => [
+          'linkit_extension' => [
+            'linkit_enabled' => TRUE,
+            'linkit_profile' => 'default',
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
+    return $data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function providerPair(): array {
+    // Linkit is 100% independent of the text format, so no need for this test.
+    return [];
+  }
+
+}
diff --git a/web/modules/linkit/webpack.config.js b/web/modules/linkit/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..afb1bbc82774b50c150bd3d3a5a85cabe7caffb4
--- /dev/null
+++ b/web/modules/linkit/webpack.config.js
@@ -0,0 +1,57 @@
+const path = require('path');
+const fs = require('fs');
+const webpack = require('webpack');
+const TerserPlugin = require('terser-webpack-plugin');
+
+function getDirectories(srcpath) {
+  return fs
+    .readdirSync(srcpath)
+    .filter((item) => fs.statSync(path.join(srcpath, item)).isDirectory());
+}
+
+module.exports = [];
+// Loop through every subdirectory in src, each a different plugin, and build
+// each one in ./build.
+getDirectories('./js/ckeditor5_plugins').forEach((dir) => {
+  const bc = {
+    mode: 'production',
+    optimization: {
+      minimize: true,
+      minimizer: [
+        new TerserPlugin({
+          terserOptions: {
+            format: {
+              comments: false,
+            },
+          },
+          test: /\.js(\?.*)?$/i,
+          extractComments: false,
+        }),
+      ],
+      moduleIds: 'named',
+    },
+    entry: {
+      path: path.resolve(__dirname, 'js/ckeditor5_plugins', dir, 'src/index.js')
+    },
+    output: {
+      path: path.resolve(__dirname, './js/build'),
+      filename: `${dir}.js`,
+      library: ['CKEditor5', dir],
+      libraryTarget: 'umd',
+      libraryExport: 'default'
+    },
+    plugins: [
+      new webpack.DllReferencePlugin({
+        manifest: require('./node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'), // eslint-disable-line global-require, import/no-unresolved
+        scope: 'ckeditor5/src',
+        name: 'CKEditor5.dll',
+      }),
+    ],
+    module: {
+      rules: [{ test: /\.svg$/, use: 'raw-loader' }],
+    },
+    devtool: false,
+  };
+
+  module.exports.push(bc);
+});