diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9f273dedd8fbaa5386589c0b0ae9d1c43ee0c8af..5af14491fcc2b103c6d54b206b262cb766c4069d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,7 +38,6 @@ phpstan:
 variables:
   _CSPELL_WORDS: 'Kravchuk,ptmkenny,Taras,tarik'
   OPT_IN_TEST_NEXT_MINOR: '1'
-  OPT_IN_TEST_PREV_MAJOR: '1'
 #   SKIP_ESLINT: '1'
 #   OPT_IN_TEST_NEXT_MAJOR: '1'
 #   _CURL_TEMPLATES_REF: 'main'
diff --git a/phpstan.neon b/phpstan.neon
index 6db079dfdf32debb54da85ba86a1c5f5642d084c..aa2de20e0cde4ef8f86ecc5d0cc3a909958a2c13 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -5,3 +5,5 @@ parameters:
 	ignoreErrors:
 	  # Drupal does not define its own arrays.
 	  - '#no value type specified in iterable type array#'
+	  # TranslatableMarkup is equivalent to string
+	  - '#TranslatableMarkup given#'
diff --git a/tests/src/Functional/JsonApiLinksFunctionalTest.php b/tests/src/Functional/JsonApiLinksFunctionalTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c996c4207f3eb1bb54e8a5c7a57fb20f0506afd8
--- /dev/null
+++ b/tests/src/Functional/JsonApiLinksFunctionalTest.php
@@ -0,0 +1,191 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\jsonapi_links\Exception\JsonApiLinksException;
+use Drupal\user\Entity\User;
+
+/**
+ * General functional test class.
+ *
+ * @group jsonapi_links
+ */
+class JsonApiLinksFunctionalTest extends JsonApiFunctionalTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'basic_auth',
+    'jsonapi_links',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * The module config identifier.
+   *
+   * @var string
+   */
+  protected string $configName = 'jsonapi_links.settings';
+
+  /**
+   * The name of the config for removing links.
+   *
+   * @var string
+   */
+  protected string $configRemoveLinks = 'remove_links';
+
+  /**
+   * Test admin user.
+   *
+   * @var \Drupal\user\Entity\User
+   *
+   * @todo Remove when 11.2 is released.
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Remove when 11.2 is released.
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    if (!($this->adminUser instanceof User)) {
+      $admin_user = $this->drupalCreateUser([
+        'create article content',
+        'edit any article content',
+        'delete any article content',
+      ],
+        'jsonapi_admin_user',
+        TRUE,
+      );
+      if ($admin_user === FALSE) {
+        throw new JsonApiLinksException("Failed to create admin user!");
+      }
+      $this->adminUser = $admin_user;
+    }
+  }
+
+  /**
+   * Tests link removal.
+   *
+   * Inspired by testRead() in JsonApiFunctionalTest.
+   */
+  public function testLinkRemoval(): void {
+    // JSON:API Links does not remove anything by default.
+    $this->createDefaultContent(61, 5, TRUE, TRUE, static::IS_NOT_MULTILINGUAL, FALSE);
+    // Unpublish the last entity, so we can check access.
+    $this->nodes[60]->setUnpublished()->save();
+
+    $uuid = $this->nodes[0]->uuid();
+    if (!is_string($uuid)) {
+      throw new JsonApiLinksException("Failed to get UUID for node!");
+    }
+
+    // 0. HEAD request allows a client to verify that JSON:API is installed.
+    $this->httpClient->request('HEAD', $this->buildUrl('/jsonapi/node/article'));
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Confirm the default behavior (no change from core).
+    $this->checkForLinksPresent($uuid, FALSE);
+
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('/admin/config/services/jsonapi/links');
+    $form_values = [
+      'edit-remove-links' => 1,
+    ];
+    $this->submitForm($form_values, t('Save configuration'));
+
+    // Verify that the configuration value is set.
+    $this->assertTrue($this->config($this->configName)->get($this->configRemoveLinks));
+
+    // Ensure links are removed as expected.
+    $this->checkForLinksPresent($uuid, TRUE);
+
+    // Disable link removal with JSON:API Links.
+    $this->drupalGet('/admin/config/services/jsonapi/links');
+    $form_values = [
+      'edit-remove-links' => 0,
+    ];
+    $this->submitForm($form_values, t('Save configuration'));
+
+    // Verify that the configuration value is set.
+    $this->assertFalse($this->config($this->configName)->get($this->configRemoveLinks));
+    // Logout to refresh cache.
+    $this->drupalLogout();
+
+    // Ensure that the prior behavior has been restored.
+    $this->checkForLinksPresent($uuid, FALSE);
+  }
+
+  /**
+   * Confirms that link attributes are present or absent.
+   *
+   * @param string $uuid
+   *   The uuid of the article.
+   * @param bool $links_removed
+   *   TRUE if links should be removed; FALSE otherwise.
+   */
+  private function checkForLinksPresent(string $uuid, bool $links_removed): void {
+    // 6. Single relationship item.
+    $drupal_response1 = $this->drupalGet('/jsonapi/node/article/' . $uuid);
+    $single_output = json_decode($drupal_response1, TRUE);
+    if (!is_array($single_output)) {
+      throw new JsonApiLinksException("Failed to decode JSON:API response!");
+    }
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertLinksRemoved($single_output, $links_removed);
+    // 11. Includes with relationships.
+    $drupal_response2 = $this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/uid', [
+      'query' => ['include' => 'uid'],
+    ]);
+    $output_with_relationships = json_decode($drupal_response2, TRUE);
+    if (!is_array($output_with_relationships)) {
+      throw new JsonApiLinksException("Failed to decode JSON:API response!");
+    }
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertLinksRemoved($output_with_relationships, $links_removed);
+  }
+
+  /**
+   * Asserts that links are removed or present depending on the module setting.
+   *
+   * @param array $drupal_jsonapi_response
+   *   The Drupal JSON:API response to check.
+   * @param bool $links_should_be_removed
+   *   TRUE if links attributes should be removed.
+   */
+  private function assertLinksRemoved(array $drupal_jsonapi_response, bool $links_should_be_removed): void {
+    // Merge the includes into the response data because we want to make sure
+    // that the links attribute has also been removed from includes.
+    $response_content = $drupal_jsonapi_response['data'];
+    if (!is_array($response_content)) {
+      throw new JsonApiLinksException("Failed to parse 'data' in JSON:API response!");
+    }
+    if (isset($drupal_jsonapi_response['included'])) {
+      $response_content = array_merge($response_content, $drupal_jsonapi_response['included']);
+    }
+    $first_item = json_encode($response_content);
+    if (!is_string($first_item)) {
+      throw new JsonApiLinksException("Failed to encode first item!");
+    }
+    if ($links_should_be_removed) {
+      // Links attribute should be removed from each item.
+      $this->assertDoesNotMatchRegularExpression('/links/', $first_item);
+    }
+    else {
+      $this->assertMatchesRegularExpression('/links/', $first_item);
+    }
+    // Links attribute of response should never be removed.
+    $this->assertArrayHasKey('links', $drupal_jsonapi_response);
+  }
+
+}
diff --git a/tests/src/Kernel/JsonapiLinksInstallTest.php b/tests/src/Kernel/JsonApiLinksInstallTest.php
similarity index 95%
rename from tests/src/Kernel/JsonapiLinksInstallTest.php
rename to tests/src/Kernel/JsonApiLinksInstallTest.php
index ac4420183bd5fd91bee8e0f02f84d413b57dba51..39a5fa96742e19b73b6fee76b6df5659c86a4bf3 100644
--- a/tests/src/Kernel/JsonapiLinksInstallTest.php
+++ b/tests/src/Kernel/JsonApiLinksInstallTest.php
@@ -11,7 +11,7 @@ use Drupal\KernelTests\KernelTestBase;
  *
  * @group jsonapi_links
  */
-class JsonapiLinksInstallTest extends KernelTestBase {
+class JsonApiLinksInstallTest extends KernelTestBase {
 
   private const string MODULE_NAME = 'jsonapi_links';