diff --git a/src/Hook/TocNodeHooks.php b/src/Hook/TocNodeHooks.php
new file mode 100644
index 0000000..8e4b44e
--- /dev/null
+++ b/src/Hook/TocNodeHooks.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\toc_node\Hook;
+
+use Drupal\Core\Entity\EntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Markup;
+use Drupal\Core\Url;
+use Drupal\Component\Utility\Html;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for toc_node.
+ */
+class TocNodeHooks
+{
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public function theme()
+    {
+        return [
+            'toc_node_links' => [
+                'variables' => [
+                    'links' => NULL,
+                ],
+                'template' => 'toc-node-links',
+            ],
+        ];
+    }
+    /**
+     * Implements hook_form_alter().
+     */
+    #[Hook('form_alter')]
+    public function formAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id)
+    {
+        if (in_array($form_id, [
+            'node_type_add_form',
+            'node_type_edit_form',
+        ])) {
+            _toc_node_node_type_form_alter($form, $form_state);
+            return;
+        }
+        // Match node add/edit forms (base form id 'node_form'). A generic
+        // /^node_\w+_form$/ pattern also matches the bulk-delete confirm form
+        // ('node_delete_multiple_confirm_form'), which is not an entity form.
+        if (($form_state->getBuildInfo()['base_form_id'] ?? NULL) === 'node_form') {
+            _toc_node_node_form_alter($form, $form_state);
+        }
+    }
+    /**
+     * Implements hook_node_insert().
+     */
+    #[Hook('node_insert')]
+    public function nodeInsert(\Drupal\node\NodeInterface $node)
+    {
+        if (!isset($node->toc_node_style)) {
+            return;
+        }
+        \Drupal::database()->insert('toc_node')->fields(_toc_node_build_fields($node))->execute();
+    }
+    /**
+     * Implements hook_node_update().
+     */
+    #[Hook('node_update')]
+    public function nodeUpdate(\Drupal\node\NodeInterface $node)
+    {
+        if (!isset($node->toc_node_style)) {
+            \Drupal::database()->delete('toc_node')->condition('nid', (int) $node->id())->execute();
+            return;
+        }
+        \Drupal::database()->merge('toc_node')->key('nid', (int) $node->id())->fields(_toc_node_build_fields($node))->execute();
+    }
+    /**
+     * Implements hook_node_delete().
+     */
+    #[Hook('node_delete')]
+    public function nodeDelete(\Drupal\node\NodeInterface $node)
+    {
+        \Drupal::database()->delete('toc_node')->condition('nid', (int) $node->id())->execute();
+    }
+    /**
+     * Implements hook_node_load().
+     */
+    #[Hook('node_load')]
+    public function nodeLoad(array $entities)
+    {
+        $type_settings_cache = [
+        ];
+        $nids = [
+        ];
+        foreach ($entities as $node) {
+            $bundle = $node->bundle();
+            if (!isset($type_settings_cache[$bundle])) {
+                $settings = _toc_node_type_settings($bundle);
+                $type_settings_cache[$bundle] = $settings['enabled'] ?? FALSE;
+            }
+            if (empty($type_settings_cache[$bundle])) {
+                continue;
+            }
+            $nids[] = $node->id();
+        }
+        if (empty($nids)) {
+            return;
+        }
+        $records = \Drupal::database()->select('toc_node', 'tn')->fields('tn')->condition('nid', $nids, 'IN')->execute()->fetchAllAssoc('nid');
+        foreach ($entities as $node) {
+            $nid = (int) $node->id();
+            if (isset($records[$nid])) {
+                $record = $records[$nid];
+                $node->toc_node_style = $record->style;
+                $node->toc_node_level = $record->level;
+                $node->toc_node_back_to_top_links = $record->back_links;
+            }
+        }
+    }
+    /**
+     * Implements hook_node_view().
+     */
+    #[Hook('node_view')]
+    public function nodeView(array &$build, \Drupal\node\NodeInterface $node, $display, $view_mode)
+    {
+        if ($view_mode !== 'full') {
+            return;
+        }
+        $type_settings = _toc_node_type_settings($node->bundle());
+        if (empty($type_settings['enabled'])) {
+            return;
+        }
+        $style = $node->toc_node_style ?? $type_settings['style_default'];
+        $route_match = \Drupal::routeMatch();
+        if ($route_match->getRouteName() !== 'entity.node.canonical') {
+            return;
+        }
+        $current = $route_match->getParameter('node');
+        if (!$current instanceof \Drupal\node\NodeInterface || $current->id() !== $node->id()) {
+            return;
+        }
+        $build['#attached']['library'][] = 'toc_node/toc_node';
+        $back_to_top = _toc_node_back_to_top($node, $type_settings);
+        if ($back_to_top) {
+            $build['#attached']['library'][] = 'toc_node/back_to_top';
+        }
+        // The post-render TOC output depends on the current request path (links use
+        // the path alias), so the node render must vary per URL.
+        $build['#cache']['contexts'][] = 'url.path';
+        $build['#toc_node_style'] = _toc_node_valid_style($style);
+        $build['#post_render'][] = '\Drupal\toc_node\Render\TocNodeRenderCallbacks::postRender';
+    }
+}
diff --git a/tests/src/Functional/TocNodeAdminTest.php b/tests/src/Functional/TocNodeAdminTest.php
index bba8dbd..9c2f8c0 100644
--- a/tests/src/Functional/TocNodeAdminTest.php
+++ b/tests/src/Functional/TocNodeAdminTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\node\Entity\NodeType;
 
@@ -12,6 +14,8 @@ use Drupal\node\Entity\NodeType;
  *
  * @group toc_node
  */
+#[Group('toc_node')]
+#[RunTestsInSeparateProcesses]
 class TocNodeAdminTest extends BrowserTestBase {
 
   /**
diff --git a/tests/src/Functional/TocNodeBlockTest.php b/tests/src/Functional/TocNodeBlockTest.php
index c166391..ff5f13a 100644
--- a/tests/src/Functional/TocNodeBlockTest.php
+++ b/tests/src/Functional/TocNodeBlockTest.php
@@ -4,6 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\node\Entity\Node;
@@ -14,6 +17,8 @@ use Drupal\node\Entity\NodeType;
  *
  * @group toc_node
  */
+#[Group('toc_node')]
+#[RunTestsInSeparateProcesses]
 class TocNodeBlockTest extends BrowserTestBase {
 
   /**
@@ -78,7 +83,7 @@ class TocNodeBlockTest extends BrowserTestBase {
     $node_type->setThirdPartySetting('toc_node', 'style_default', 'bullets');
     $node_type->save();
 
-    node_add_body_field($node_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $node_type->id()), fn() => node_add_body_field($node_type));
 
     \Drupal::service('entity_display.repository')
       ->getViewDisplay('node', 'page', 'full')
@@ -199,7 +204,7 @@ class TocNodeBlockTest extends BrowserTestBase {
     $node_type->setThirdPartySetting('toc_node', 'styles', ['none', 'bullets', 'numbers']);
     $node_type->setThirdPartySetting('toc_node', 'style_default', 'bullets');
     $node_type->save();
-    node_add_body_field($node_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $node_type->id()), fn() => node_add_body_field($node_type));
 
     \Drupal::service('entity_display.repository')
       ->getViewDisplay('node', 'doc', 'full')
diff --git a/tests/src/Functional/TocNodeNodeViewTest.php b/tests/src/Functional/TocNodeNodeViewTest.php
index 659e3bf..dddf125 100644
--- a/tests/src/Functional/TocNodeNodeViewTest.php
+++ b/tests/src/Functional/TocNodeNodeViewTest.php
@@ -4,6 +4,9 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\node\Entity\NodeType;
@@ -13,6 +16,8 @@ use Drupal\node\Entity\NodeType;
  *
  * @group toc_node
  */
+#[Group('toc_node')]
+#[RunTestsInSeparateProcesses]
 class TocNodeNodeViewTest extends BrowserTestBase {
 
   /**
@@ -80,7 +85,7 @@ class TocNodeNodeViewTest extends BrowserTestBase {
     $node_type->setThirdPartySetting('toc_node', 'style_default', 'bullets');
     $node_type->save();
 
-    node_add_body_field($node_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $node_type->id()), fn() => node_add_body_field($node_type));
 
     \Drupal::service('entity_display.repository')
       ->getViewDisplay('node', $type, 'full')
@@ -127,7 +132,7 @@ class TocNodeNodeViewTest extends BrowserTestBase {
       'name' => 'Page',
     ]);
     $node_type->save();
-    node_add_body_field($node_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $node_type->id()), fn() => node_add_body_field($node_type));
 
     \Drupal::service('entity_display.repository')
       ->getViewDisplay('node', 'page', 'full')
@@ -186,7 +191,7 @@ class TocNodeNodeViewTest extends BrowserTestBase {
     $node_type->setThirdPartySetting('toc_node', 'styles', ['none', 'bullets', 'numbers']);
     $node_type->setThirdPartySetting('toc_node', 'style_default', 'bullets');
     $node_type->save();
-    node_add_body_field($node_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $node_type->id()), fn() => node_add_body_field($node_type));
 
     \Drupal::service('entity_display.repository')
       ->getViewDisplay('node', 'page', 'full')
diff --git a/tests/src/Kernel/TocNodeGenerateTest.php b/tests/src/Kernel/TocNodeGenerateTest.php
index c5e10bd..a9d5b60 100644
--- a/tests/src/Kernel/TocNodeGenerateTest.php
+++ b/tests/src/Kernel/TocNodeGenerateTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Render\RenderContext;
 use Drupal\KernelTests\KernelTestBase;
 
@@ -12,6 +14,8 @@ use Drupal\KernelTests\KernelTestBase;
  *
  * @group toc_node
  */
+#[Group('toc_node')]
+#[RunTestsInSeparateProcesses]
 class TocNodeGenerateTest extends KernelTestBase {
 
   /**
diff --git a/tests/src/Kernel/TocNodeSchemaTest.php b/tests/src/Kernel/TocNodeSchemaTest.php
index a91ed30..55e54f5 100644
--- a/tests/src/Kernel/TocNodeSchemaTest.php
+++ b/tests/src/Kernel/TocNodeSchemaTest.php
@@ -4,11 +4,13 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Kernel;
 
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\KernelTests\KernelTestBase;
 
 /**
  * Tests the database schema defined by toc_node_schema().
  */
+#[RunTestsInSeparateProcesses]
 class TocNodeSchemaTest extends KernelTestBase {
 
   /**
diff --git a/tests/src/Kernel/TocNodeSettingsTest.php b/tests/src/Kernel/TocNodeSettingsTest.php
index 2f23d81..df7715c 100644
--- a/tests/src/Kernel/TocNodeSettingsTest.php
+++ b/tests/src/Kernel/TocNodeSettingsTest.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Kernel;
 
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\NodeType;
 
 /**
  * Tests _toc_node_type_settings() and third-party settings persistence.
  */
+#[RunTestsInSeparateProcesses]
 class TocNodeSettingsTest extends KernelTestBase {
 
   /**
diff --git a/tests/src/Unit/TocNodeFormAlterTest.php b/tests/src/Unit/TocNodeFormAlterTest.php
index 6311679..b308ce1 100644
--- a/tests/src/Unit/TocNodeFormAlterTest.php
+++ b/tests/src/Unit/TocNodeFormAlterTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\toc_node\Unit;
 
+use PHPUnit\Framework\Attributes\Group;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Tests\UnitTestCase;
 
@@ -16,6 +17,7 @@ use Drupal\Tests\UnitTestCase;
  *
  * @group toc_node
  */
+#[Group('toc_node')]
 class TocNodeFormAlterTest extends UnitTestCase {
 
   /**
diff --git a/toc_node.info.yml b/toc_node.info.yml
index 7141852..b26cc2b 100644
--- a/toc_node.info.yml
+++ b/toc_node.info.yml
@@ -1,7 +1,7 @@
 name: 'TOC Node'
 type: module
 description: 'Provides Table Of Contents for nodes.'
-core_version_requirement: ^10 || ^11
+core_version_requirement: ^11.3 || ^12
 package: 'Content'
 dependencies:
   - drupal:node
diff --git a/toc_node.module b/toc_node.module
index 44a9415..baba21f 100755
--- a/toc_node.module
+++ b/toc_node.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\toc_node\Hook\TocNodeHooks;
 use Drupal\Core\Entity\EntityFormInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Markup;
@@ -10,30 +12,18 @@ use Drupal\node\NodeInterface;
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function toc_node_theme() {
-  return [
-    'toc_node_links' => [
-      'variables' => ['links' => NULL],
-      'template' => 'toc-node-links',
-    ],
-  ];
+  return \Drupal::service(TocNodeHooks::class)->theme();
 }
 
 /**
  * Implements hook_form_alter().
  */
-function toc_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  if (in_array($form_id, ['node_type_add_form', 'node_type_edit_form'])) {
-    _toc_node_node_type_form_alter($form, $form_state);
-    return;
-  }
-
-  // Match node add/edit forms (base form id 'node_form'). A generic
-  // /^node_\w+_form$/ pattern also matches the bulk-delete confirm form
-  // ('node_delete_multiple_confirm_form'), which is not an entity form.
-  if (($form_state->getBuildInfo()['base_form_id'] ?? NULL) === 'node_form') {
-    _toc_node_node_form_alter($form, $form_state);
-  }
+#[LegacyHook]
+function toc_node_form_alter(&$form, FormStateInterface $form_state, $form_id)
+{
+    \Drupal::service(TocNodeHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
@@ -310,121 +300,45 @@ function _toc_node_back_to_top(NodeInterface $node, array $type_settings): int {
 /**
  * Implements hook_node_insert().
  */
-function toc_node_node_insert(NodeInterface $node) {
-  if (!isset($node->toc_node_style)) {
-    return;
-  }
-
-  \Drupal::database()->insert('toc_node')
-    ->fields(_toc_node_build_fields($node))
-    ->execute();
+#[LegacyHook]
+function toc_node_node_insert(NodeInterface $node)
+{
+    \Drupal::service(TocNodeHooks::class)->nodeInsert($node);
 }
 
 /**
  * Implements hook_node_update().
  */
-function toc_node_node_update(NodeInterface $node) {
-  if (!isset($node->toc_node_style)) {
-    \Drupal::database()->delete('toc_node')
-      ->condition('nid', (int) $node->id())
-      ->execute();
-    return;
-  }
-
-  \Drupal::database()->merge('toc_node')
-    ->key('nid', (int) $node->id())
-    ->fields(_toc_node_build_fields($node))
-    ->execute();
+#[LegacyHook]
+function toc_node_node_update(NodeInterface $node)
+{
+    \Drupal::service(TocNodeHooks::class)->nodeUpdate($node);
 }
 
 /**
  * Implements hook_node_delete().
  */
+#[LegacyHook]
 function toc_node_node_delete(NodeInterface $node) {
-  \Drupal::database()->delete('toc_node')
-    ->condition('nid', (int) $node->id())
-    ->execute();
+  \Drupal::service(TocNodeHooks::class)->nodeDelete($node);
 }
 
 /**
  * Implements hook_node_load().
  */
-function toc_node_node_load(array $entities) {
-  $type_settings_cache = [];
-  $nids = [];
-
-  foreach ($entities as $node) {
-    $bundle = $node->bundle();
-    if (!isset($type_settings_cache[$bundle])) {
-      $settings = _toc_node_type_settings($bundle);
-      $type_settings_cache[$bundle] = $settings['enabled'] ?? FALSE;
-    }
-    if (empty($type_settings_cache[$bundle])) {
-      continue;
-    }
-
-    $nids[] = $node->id();
-  }
-
-  if (empty($nids)) {
-    return;
-  }
-
-  $records = \Drupal::database()->select('toc_node', 'tn')
-    ->fields('tn')
-    ->condition('nid', $nids, 'IN')
-    ->execute()
-    ->fetchAllAssoc('nid');
-
-  foreach ($entities as $node) {
-    $nid = (int) $node->id();
-    if (isset($records[$nid])) {
-      $record = $records[$nid];
-      $node->toc_node_style = $record->style;
-      $node->toc_node_level = $record->level;
-      $node->toc_node_back_to_top_links = $record->back_links;
-    }
-  }
+#[LegacyHook]
+function toc_node_node_load(array $entities)
+{
+    \Drupal::service(TocNodeHooks::class)->nodeLoad($entities);
 }
 
 /**
  * Implements hook_node_view().
  */
-function toc_node_node_view(array &$build, NodeInterface $node, $display, $view_mode) {
-  if ($view_mode !== 'full') {
-    return;
-  }
-
-  $type_settings = _toc_node_type_settings($node->bundle());
-  if (empty($type_settings['enabled'])) {
-    return;
-  }
-
-  $style = $node->toc_node_style ?? $type_settings['style_default'];
-
-  $route_match = \Drupal::routeMatch();
-  if ($route_match->getRouteName() !== 'entity.node.canonical') {
-    return;
-  }
-
-  $current = $route_match->getParameter('node');
-  if (!$current instanceof NodeInterface || $current->id() !== $node->id()) {
-    return;
-  }
-
-  $build['#attached']['library'][] = 'toc_node/toc_node';
-
-  $back_to_top = _toc_node_back_to_top($node, $type_settings);
-  if ($back_to_top) {
-    $build['#attached']['library'][] = 'toc_node/back_to_top';
-  }
-
-  // The post-render TOC output depends on the current request path (links use
-  // the path alias), so the node render must vary per URL.
-  $build['#cache']['contexts'][] = 'url.path';
-
-  $build['#toc_node_style'] = _toc_node_valid_style($style);
-  $build['#post_render'][] = '\Drupal\toc_node\Render\TocNodeRenderCallbacks::postRender';
+#[LegacyHook]
+function toc_node_node_view(array &$build, NodeInterface $node, $display, $view_mode)
+{
+    \Drupal::service(TocNodeHooks::class)->nodeView($build, $node, $display, $view_mode);
 }
 
 /**
diff --git a/toc_node.services.yml b/toc_node.services.yml
new file mode 100644
index 0000000..402e10b
--- /dev/null
+++ b/toc_node.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\toc_node\Hook\TocNodeHooks:
+    class: Drupal\toc_node\Hook\TocNodeHooks
+    autowire: true
