diff --git a/prototype.services.yml b/prototype.services.yml
new file mode 100644
index 0000000..aa893fd
--- /dev/null
+++ b/prototype.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\prototype\Hook\PrototypeHooks:
+    class: Drupal\prototype\Hook\PrototypeHooks
+    autowire: true
diff --git a/prototype.theme b/prototype.theme
index e1175bf..5f7b2bf 100755
--- a/prototype.theme
+++ b/prototype.theme
@@ -5,12 +5,12 @@
  * Functions to support theming.
  */
 
-use Drupal\block_content\BlockContentInterface;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\prototype\Hook\PrototypeHooks;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Extension\ThemeSettingsProvider;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
-use Drupal\views\ViewExecutable;
 
 /**
  * Get a theme setting value.
@@ -37,114 +37,57 @@ function prototype_get_theme_setting($setting_name, $default = NULL) {
 /**
  * Implements hook_preprocess_HOOK() for html.html.twig.
  */
+#[LegacyHook]
 function prototype_preprocess_html(&$variables) {
-  $slide_direction = prototype_get_theme_setting('mobile_menu_slide_direction');
-  if ($slide_direction) {
-    $variables['attributes']['class'][] = $slide_direction;
-  }
+  \Drupal::service(PrototypeHooks::class)->preprocessHtml($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_checkboxes(&$variables) {
-  $variables['attributes']['class'][] = 'form-boolean-group';
+  \Drupal::service(PrototypeHooks::class)->preprocessCheckboxes($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_field(&$variables) {
-  $rich_field_types = ['text_with_summary', 'text', 'text_long'];
-
-  if (in_array($variables['field_type'], $rich_field_types, TRUE)) {
-    $variables['attributes']['class'][] = 'text-content';
-  }
+  \Drupal::service(PrototypeHooks::class)->preprocessField($variables);
 }
 
 /**
  * Implements hook_preprocess_form_element().
  */
+#[LegacyHook]
 function prototype_preprocess_form_element(&$variables) {
-  if (in_array($variables['element']['#type'] ?? FALSE, ['checkbox', 'radio'], TRUE)) {
-    $variables['attributes']['class'][] = 'form-type-boolean';
-  }
-
-  if (!empty($variables['description']['attributes'])) {
-    $variables['description']['attributes']->addClass('form-item__description');
-  }
-
-  if ($variables['disabled']) {
-    $variables['label']['#attributes']['class'][] = 'is-disabled';
-  }
+  \Drupal::service(PrototypeHooks::class)->preprocessFormElement($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_form_element_label(&$variables) {
-  $variables['attributes']['class'][] = 'form-item__label';
+  \Drupal::service(PrototypeHooks::class)->preprocessFormElementLabel($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_input(&$variables) {
-  if (
-    !empty($variables['element']['#title_display']) &&
-    $variables['element']['#title_display'] === 'attribute' &&
-    !empty((string) $variables['element']['#title'])
-  ) {
-    $variables['attributes']['title'] = (string) $variables['element']['#title'];
-  }
-
-  $type_api = $variables['element']['#type'];
-  $type_html = $variables['attributes']['type'];
-  $text_types_html = [
-    'text',
-    'email',
-    'tel',
-    'number',
-    'search',
-    'password',
-    'date',
-    'time',
-    'file',
-    'color',
-    'datetime-local',
-    'url',
-    'month',
-    'week',
-  ];
-
-  if (in_array($type_html, $text_types_html, TRUE)) {
-    $variables['attributes']['class'][] = 'form-element';
-    $variables['attributes']['class'][] = Html::getClass('form-element--type-' . $type_html);
-    $variables['attributes']['class'][] = Html::getClass('form-element--api-' . $type_api);
-
-    if (!empty($variables['element']['#autocomplete_route_name'])) {
-      $variables['autocomplete_message'] = t('Loading…');
-    }
-  }
-
-  if (in_array($type_html, ['checkbox', 'radio'], TRUE)) {
-    $variables['attributes']['class'][] = 'form-boolean';
-    $variables['attributes']['class'][] = Html::getClass('form-boolean--type-' . $type_html);
-  }
+  \Drupal::service(PrototypeHooks::class)->preprocessInput($variables);
 }
 
 /**
  * Implements hook_preprocess_menu().
  */
+#[LegacyHook]
 function prototype_preprocess_menu(&$variables) {
-  $menu_name = $variables['menu_name'];
-  $attributes = &$variables['attributes'];
-
-  // Add base classes to our menu.
-  $attributes['class'][] = 'menu';
-  $attributes['class'][] = "menu--{$menu_name}";
-
-  prototype_prepare_menu_items($variables['items']);
+  \Drupal::service(PrototypeHooks::class)->preprocessMenu($variables);
 }
 
 /**
@@ -233,21 +176,17 @@ function prototype_prepare_menu_url(Url $url): Url {
 /**
  * Implements hook_preprocess_menu_link_content().
  */
+#[LegacyHook]
 function prototype_preprocess_menu_link_content(&$variables): void {
-  $menu_item = $variables['menu_link_content'];
-
-  $url = prototype_prepare_menu_url($menu_item->getUrlObject());
-
-  $variables['title'] = $menu_item->getTitle();
-  $variables['url'] = $url;
-  $variables['link_type'] = $url->getOption('link_type') ?? '';
+  \Drupal::service(PrototypeHooks::class)->preprocessMenuLinkContent($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_radios(&$variables) {
-  $variables['attributes']['class'][] = 'form-boolean-group';
+  \Drupal::service(PrototypeHooks::class)->preprocessRadios($variables);
 }
 
 /**
@@ -274,31 +213,25 @@ function prototype_preprocess_region__footer(array &$variables) {
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_select(&$variables) {
-  $variables['attributes']['class'][] = 'form-element';
-  $variables['attributes']['class'][] = $variables['element']['#multiple'] ?
-    'form-element--type-select-multiple' :
-    'form-element--type-select';
+  \Drupal::service(PrototypeHooks::class)->preprocessSelect($variables);
 }
 
 /**
  * Implements hook_preprocess_HOOK().
  */
+#[LegacyHook]
 function prototype_preprocess_textarea(&$variables) {
-  $variables['attributes']['class'][] = 'form-element';
-  $variables['attributes']['class'][] = 'form-element--type-textarea';
-  $variables['attributes']['class'][] = 'form-element--api-textarea';
+  \Drupal::service(PrototypeHooks::class)->preprocessTextarea($variables);
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK_alter().
  */
+#[LegacyHook]
 function prototype_theme_suggestions_block_alter(array &$suggestions, array $variables) {
-  $content = $variables['elements']['content'];
-
-  if (isset($content['#block_content']) && $content['#block_content'] instanceof BlockContentInterface) {
-    $suggestions[] = 'block__block_content__' . $content['#block_content']->bundle();
-  }
+  \Drupal::service(PrototypeHooks::class)->themeSuggestionsBlockAlter($suggestions, $variables);
 }
 
 /**
@@ -318,9 +251,9 @@ function prototype_theme_suggestions_input_alter(&$suggestions, array $variables
 /**
  * Implements hook_theme_suggestions_HOOK_alter().
  */
+#[LegacyHook]
 function prototype_theme_suggestions_taxonomy_term_alter(array &$suggestions, array $variables) {
-  $view_mode = $variables['elements']['#view_mode'];
-  $suggestions[] = 'taxonomy_term__' . $view_mode;
+  \Drupal::service(PrototypeHooks::class)->themeSuggestionsTaxonomyTermAlter($suggestions, $variables);
 }
 
 /**
@@ -330,27 +263,15 @@ function prototype_theme_suggestions_taxonomy_term_alter(array &$suggestions, ar
  * https://www.drupal.org/project/drupal/issues/2270883
  * https://www.drupal.org/project/drupal/issues/2766379
  */
+#[LegacyHook]
 function prototype_theme_suggestions_user_alter(array &$suggestions, array $variables) {
-  $view_mode = $variables['elements']['#view_mode'] ?? '';
-  if ($view_mode) {
-    $suggestions[] = 'user__' . $view_mode;
-  }
+  \Drupal::service(PrototypeHooks::class)->themeSuggestionsUserAlter($suggestions, $variables);
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK_alter().
  */
+#[LegacyHook]
 function prototype_theme_suggestions_views_view_alter(array &$suggestions, array $variables) {
-  $view = $variables['view'];
-
-  if (
-    $view instanceof ViewExecutable
-    && $display = $view->current_display
-  ) {
-    $view_id = $view->id();
-    $original = $variables['theme_hook_original'];
-
-    $suggestions[] = "{$original}__$view_id";
-    $suggestions[] = "{$original}__{$view_id}__$display";
-  }
+  \Drupal::service(PrototypeHooks::class)->themeSuggestionsViewsViewAlter($suggestions, $variables);
 }
diff --git a/src/Hook/PrototypeHooks.php b/src/Hook/PrototypeHooks.php
new file mode 100644
index 0000000..378bf55
--- /dev/null
+++ b/src/Hook/PrototypeHooks.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\prototype\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\block_content\BlockContentInterface;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for prototype.
+ */
+class PrototypeHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_preprocess_HOOK() for html.html.twig.
+   */
+  #[Hook('preprocess_html')]
+  public static function preprocessHtml(&$variables) {
+    $slide_direction = prototype_get_theme_setting('mobile_menu_slide_direction');
+    if ($slide_direction) {
+      $variables['attributes']['class'][] = $slide_direction;
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_checkboxes')]
+  public static function preprocessCheckboxes(&$variables) {
+    $variables['attributes']['class'][] = 'form-boolean-group';
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_field')]
+  public static function preprocessField(&$variables) {
+    $rich_field_types = [
+      'text_with_summary',
+      'text',
+      'text_long',
+    ];
+    if (in_array($variables['field_type'], $rich_field_types, TRUE)) {
+      $variables['attributes']['class'][] = 'text-content';
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_form_element().
+   */
+  #[Hook('preprocess_form_element')]
+  public static function preprocessFormElement(&$variables) {
+    if (in_array($variables['element']['#type'] ?? FALSE, [
+      'checkbox',
+      'radio',
+    ], TRUE)) {
+      $variables['attributes']['class'][] = 'form-type-boolean';
+    }
+    if (!empty($variables['description']['attributes'])) {
+      $variables['description']['attributes']->addClass('form-item__description');
+    }
+    if ($variables['disabled']) {
+      $variables['label']['#attributes']['class'][] = 'is-disabled';
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_form_element_label')]
+  public static function preprocessFormElementLabel(&$variables) {
+    $variables['attributes']['class'][] = 'form-item__label';
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_input')]
+  public function preprocessInput(&$variables) {
+    if (!empty($variables['element']['#title_display']) && $variables['element']['#title_display'] === 'attribute' && !empty((string) $variables['element']['#title'])) {
+      $variables['attributes']['title'] = (string) $variables['element']['#title'];
+    }
+    $type_api = $variables['element']['#type'];
+    $type_html = $variables['attributes']['type'];
+    $text_types_html = [
+      'text',
+      'email',
+      'tel',
+      'number',
+      'search',
+      'password',
+      'date',
+      'time',
+      'file',
+      'color',
+      'datetime-local',
+      'url',
+      'month',
+      'week',
+    ];
+    if (in_array($type_html, $text_types_html, TRUE)) {
+      $variables['attributes']['class'][] = 'form-element';
+      $variables['attributes']['class'][] = Html::getClass('form-element--type-' . $type_html);
+      $variables['attributes']['class'][] = Html::getClass('form-element--api-' . $type_api);
+      if (!empty($variables['element']['#autocomplete_route_name'])) {
+        $variables['autocomplete_message'] = $this->t('Loading…');
+      }
+    }
+    if (in_array($type_html, [
+      'checkbox',
+      'radio',
+    ], TRUE)) {
+      $variables['attributes']['class'][] = 'form-boolean';
+      $variables['attributes']['class'][] = Html::getClass('form-boolean--type-' . $type_html);
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_menu().
+   */
+  #[Hook('preprocess_menu')]
+  public static function preprocessMenu(&$variables) {
+    $menu_name = $variables['menu_name'];
+    $attributes =& $variables['attributes'];
+    // Add base classes to our menu.
+    $attributes['class'][] = 'menu';
+    $attributes['class'][] = "menu--{$menu_name}";
+    prototype_prepare_menu_items($variables['items']);
+  }
+
+  /**
+   * Implements hook_preprocess_menu_link_content().
+   */
+  #[Hook('preprocess_menu_link_content')]
+  public static function preprocessMenuLinkContent(&$variables): void {
+    $menu_item = $variables['menu_link_content'];
+    $url = prototype_prepare_menu_url($menu_item->getUrlObject());
+    $variables['title'] = $menu_item->getTitle();
+    $variables['url'] = $url;
+    $variables['link_type'] = $url->getOption('link_type') ?? '';
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_radios')]
+  public static function preprocessRadios(&$variables) {
+    $variables['attributes']['class'][] = 'form-boolean-group';
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_select')]
+  public static function preprocessSelect(&$variables) {
+    $variables['attributes']['class'][] = 'form-element';
+    $variables['attributes']['class'][] = $variables['element']['#multiple'] ? 'form-element--type-select-multiple' : 'form-element--type-select';
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK().
+   */
+  #[Hook('preprocess_textarea')]
+  public static function preprocessTextarea(&$variables) {
+    $variables['attributes']['class'][] = 'form-element';
+    $variables['attributes']['class'][] = 'form-element--type-textarea';
+    $variables['attributes']['class'][] = 'form-element--api-textarea';
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_block_alter')]
+  public static function themeSuggestionsBlockAlter(array &$suggestions, array $variables) {
+    $content = $variables['elements']['content'];
+    if (isset($content['#block_content']) && $content['#block_content'] instanceof BlockContentInterface) {
+      $suggestions[] = 'block__block_content__' . $content['#block_content']->bundle();
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_taxonomy_term_alter')]
+  public static function themeSuggestionsTaxonomyTermAlter(array &$suggestions, array $variables) {
+    $view_mode = $variables['elements']['#view_mode'];
+    $suggestions[] = 'taxonomy_term__' . $view_mode;
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   *
+   * @todo remove after these are resolved:
+   * https://www.drupal.org/project/drupal/issues/2270883
+   * https://www.drupal.org/project/drupal/issues/2766379
+   */
+  #[Hook('theme_suggestions_user_alter')]
+  public static function themeSuggestionsUserAlter(array &$suggestions, array $variables) {
+    $view_mode = $variables['elements']['#view_mode'] ?? '';
+    if ($view_mode) {
+      $suggestions[] = 'user__' . $view_mode;
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_views_view_alter')]
+  public static function themeSuggestionsViewsViewAlter(array &$suggestions, array $variables) {
+    $view = $variables['view'];
+    if ($view instanceof ViewExecutable && $display = $view->current_display) {
+      $view_id = $view->id();
+      $original = $variables['theme_hook_original'];
+      $suggestions[] = "{$original}__{$view_id}";
+      $suggestions[] = "{$original}__{$view_id}__{$display}";
+    }
+  }
+
+}
diff --git a/tests/modules/prototype_slideshow_test/prototype_slideshow_test.install b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.install
index 844d291..1977043 100644
--- a/tests/modules/prototype_slideshow_test/prototype_slideshow_test.install
+++ b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.install
@@ -5,6 +5,8 @@
  * Install hooks for the Prototype Slideshow Test module.
  */
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\File\FileExists;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\File\FileSystemInterface;
@@ -289,7 +291,7 @@ function prototype_slideshow_test_add_content_field(): void {
       'display_submitted' => FALSE,
     ]);
     $page_type->save();
-    node_add_body_field($page_type);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $this->createBodyField('node', $page_type->id()), fn() => node_add_body_field($page_type));
   }
 
   prototype_slideshow_test_create_field_storage('node', 'field_content', 'entity_reference_revisions', [
@@ -495,7 +497,7 @@ function prototype_slideshow_test_create_placeholder_media(): void {
   }
 
   $destination = $destination_dir . '/placeholder.png';
-  $copied_uri = $file_system->copy($source, $destination, FileSystemInterface::EXISTS_REPLACE);
+  $copied_uri = $file_system->copy($source, $destination, FileExists::Replace);
   $file_uri = $copied_uri ?: $destination;
 
   $file_storage = \Drupal::entityTypeManager()->getStorage('file');
diff --git a/tests/modules/prototype_slideshow_test/prototype_slideshow_test.module b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.module
index bc688a0..59cac16 100644
--- a/tests/modules/prototype_slideshow_test/prototype_slideshow_test.module
+++ b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.module
@@ -5,123 +5,33 @@
  * Prototype Slideshow Test module.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\prototype_slideshow_test\Hook\PrototypeSlideshowTestHooks;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Template\Attribute;
-use Drupal\paragraphs\Entity\Paragraph;
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function prototype_slideshow_test_theme() {
-  return [
-    'paragraph__slideshow' => [
-      'template' => 'paragraph--slideshow',
-      'base hook' => 'paragraph',
-    ],
-  ];
+  return \Drupal::service(PrototypeSlideshowTestHooks::class)->theme();
 }
 
 /**
  * Implements hook_theme_suggestions_paragraph_alter().
  */
+#[LegacyHook]
 function prototype_slideshow_test_theme_suggestions_paragraph_alter(array &$suggestions, array $variables): void {
-  if (empty($variables['elements']['#paragraph'])) {
-    return;
-  }
-  
-  $paragraph = $variables['elements']['#paragraph'];
-  if ($paragraph->bundle() === 'slideshow') {
-    $suggestions[] = 'paragraph__slideshow';
-  }
+  \Drupal::service(PrototypeSlideshowTestHooks::class)->themeSuggestionsParagraphAlter($suggestions, $variables);
 }
 
 /**
  * Implements hook_entity_view().
  */
+#[LegacyHook]
 function prototype_slideshow_test_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, string $view_mode): void {
-  if ($entity->getEntityTypeId() !== 'paragraph' || $entity->bundle() !== 'slideshow') {
-    return;
-  }
-
-  // Check entity access permissions.
-  if (!$entity->access('view', NULL, TRUE)->isAllowed()) {
-    return;
-  }
-
-  $slides = [];
-
-  // Check if slides field exists and has content.
-  $slides_field = $entity->get('field_slides');
-  if ($slides_field->isEmpty()) {
-    \Drupal::logger('prototype_slideshow_test')->warning('Slideshow paragraph @id has no slides configured.', [
-      '@id' => $entity->id(),
-    ]);
-    return;
-  }
-
-  // Get view builder once outside the loop for efficiency.
-  $view_builder = \Drupal::entityTypeManager()->getViewBuilder('paragraph');
-
-  /** @var \Drupal\paragraphs\Entity\Paragraph $slide_paragraph */
-  foreach ($slides_field->referencedEntities() as $slide_paragraph) {
-    if (!$slide_paragraph instanceof Paragraph) {
-      \Drupal::logger('prototype_slideshow_test')->warning('Skipped non-Paragraph entity in slideshow @id', [
-        '@id' => $entity->id(),
-      ]);
-      continue;
-    }
-
-    try {
-      // Get the slide paragraph view.
-      $slide_view = $view_builder->view($slide_paragraph, 'default');
-
-      // Replace the media field in the slide with clean image display.
-      $image_field = $slide_paragraph->get('field_slide_image');
-      if (!$image_field->isEmpty()) {
-        $media_entity = $image_field->entity;
-        if ($media_entity && $media_entity->hasField('field_media_image')) {
-          // Render only the image field directly from the media entity.
-          $media_image_field = $media_entity->get('field_media_image');
-          if (!$media_image_field->isEmpty()) {
-            $slide_view['field_slide_image'] = $media_image_field->view([
-              'type' => 'image',
-              'settings' => [
-                'image_style' => 'large',
-                'image_loading' => ['attribute' => 'lazy'],
-              ],
-              'label' => 'hidden',
-            ]);
-          }
-        }
-      }
-
-      $slides[] = [
-        'attributes' => new Attribute([]),
-        'content' => $slide_view,
-      ];
-    }
-    catch (\Exception $e) {
-      \Drupal::logger('prototype_slideshow_test')->error('Error processing slide @slide_id in slideshow @slideshow_id: @error', [
-        '@slide_id' => $slide_paragraph->id(),
-        '@slideshow_id' => $entity->id(),
-        '@error' => $e->getMessage(),
-      ]);
-      // Continue processing other slides despite this error.
-      continue;
-    }
-  }
-
-  // Store slides data for preprocess function.
-  // Don't override #theme - let the theme suggestion system handle it.
-  $build['#slideshow_slides'] = $slides;
-
-  // Add cache tags for proper cache invalidation.
-  $build['#cache']['tags'] = array_merge(
-    $build['#cache']['tags'] ?? [],
-    $entity->getCacheTags(),
-    ['paragraph_slideshow']
-  );
+  \Drupal::service(PrototypeSlideshowTestHooks::class)->entityView($build, $entity, $display, $view_mode);
 }
 
 /**
diff --git a/tests/modules/prototype_slideshow_test/prototype_slideshow_test.services.yml b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.services.yml
new file mode 100644
index 0000000..829aee4
--- /dev/null
+++ b/tests/modules/prototype_slideshow_test/prototype_slideshow_test.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\prototype_slideshow_test\Hook\PrototypeSlideshowTestHooks:
+    class: Drupal\prototype_slideshow_test\Hook\PrototypeSlideshowTestHooks
+    autowire: true
diff --git a/tests/modules/prototype_slideshow_test/src/Hook/PrototypeSlideshowTestHooks.php b/tests/modules/prototype_slideshow_test/src/Hook/PrototypeSlideshowTestHooks.php
new file mode 100644
index 0000000..3f1fea4
--- /dev/null
+++ b/tests/modules/prototype_slideshow_test/src/Hook/PrototypeSlideshowTestHooks.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\prototype_slideshow_test\Hook;
+
+use Drupal\Core\Template\Attribute;
+use Drupal\paragraphs\Entity\Paragraph;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for prototype_slideshow_test.
+ */
+class PrototypeSlideshowTestHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public static function theme() {
+    return [
+      'paragraph__slideshow' => [
+        'template' => 'paragraph--slideshow',
+        'base hook' => 'paragraph',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_theme_suggestions_paragraph_alter().
+   */
+  #[Hook('theme_suggestions_paragraph_alter')]
+  public static function themeSuggestionsParagraphAlter(array &$suggestions, array $variables): void {
+    if (empty($variables['elements']['#paragraph'])) {
+      return;
+    }
+    $paragraph = $variables['elements']['#paragraph'];
+    if ($paragraph->bundle() === 'slideshow') {
+      $suggestions[] = 'paragraph__slideshow';
+    }
+  }
+
+  /**
+   * Implements hook_entity_view().
+   */
+  #[Hook('entity_view')]
+  public static function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, string $view_mode): void {
+    if ($entity->getEntityTypeId() !== 'paragraph' || $entity->bundle() !== 'slideshow') {
+      return;
+    }
+    // Check entity access permissions.
+    if (!$entity->access('view', NULL, TRUE)->isAllowed()) {
+      return;
+    }
+    $slides = [];
+    // Check if slides field exists and has content.
+    $slides_field = $entity->get('field_slides');
+    if ($slides_field->isEmpty()) {
+      \Drupal::logger('prototype_slideshow_test')->warning('Slideshow paragraph @id has no slides configured.', [
+        '@id' => $entity->id(),
+      ]);
+      return;
+    }
+    // Get view builder once outside the loop for efficiency.
+    $view_builder = \Drupal::entityTypeManager()->getViewBuilder('paragraph');
+    /** @var \Drupal\paragraphs\Entity\Paragraph $slide_paragraph */
+    foreach ($slides_field->referencedEntities() as $slide_paragraph) {
+      if (!$slide_paragraph instanceof Paragraph) {
+        \Drupal::logger('prototype_slideshow_test')->warning('Skipped non-Paragraph entity in slideshow @id', [
+          '@id' => $entity->id(),
+        ]);
+        continue;
+      }
+      try {
+        // Get the slide paragraph view.
+        $slide_view = $view_builder->view($slide_paragraph, 'default');
+        // Replace the media field in the slide with clean image display.
+        $image_field = $slide_paragraph->get('field_slide_image');
+        if (!$image_field->isEmpty()) {
+          $media_entity = $image_field->entity;
+          if ($media_entity && $media_entity->hasField('field_media_image')) {
+            // Render only the image field directly from the media entity.
+            $media_image_field = $media_entity->get('field_media_image');
+            if (!$media_image_field->isEmpty()) {
+              $slide_view['field_slide_image'] = $media_image_field->view([
+                'type' => 'image',
+                'settings' => [
+                  'image_style' => 'large',
+                  'image_loading' => [
+                    'attribute' => 'lazy',
+                  ],
+                ],
+                'label' => 'hidden',
+              ]);
+            }
+          }
+        }
+        $slides[] = [
+          'attributes' => new Attribute([]),
+          'content' => $slide_view,
+        ];
+      }
+      catch (\Exception $e) {
+        \Drupal::logger('prototype_slideshow_test')->error('Error processing slide @slide_id in slideshow @slideshow_id: @error', [
+          '@slide_id' => $slide_paragraph->id(),
+          '@slideshow_id' => $entity->id(),
+          '@error' => $e->getMessage(),
+        ]);
+        // Continue processing other slides despite this error.
+        continue;
+      }
+    }
+    // Store slides data for preprocess function.
+    // Don't override #theme - let the theme suggestion system handle it.
+    $build['#slideshow_slides'] = $slides;
+    // Add cache tags for proper cache invalidation.
+    $build['#cache']['tags'] = array_merge($build['#cache']['tags'] ?? [], $entity->getCacheTags(), [
+      'paragraph_slideshow',
+    ]);
+  }
+
+}
