diff --git a/docs/components.md b/docs/components.md
index 9d30ae21e83809b1769e70b677e4b726b42d6452..c672f5c2b10c326bc09f10e173115fee98560bcc 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -111,7 +111,7 @@ For a `Block` to be compatible/eligible for use in XB it:
  - MUST have fully validatable block plugin settings config schema via the `FullyValidatable` constraint
  - MUST NOT have any required context (⚠️ handling contexts is still TBD in [#3485502](https://www.drupal.org/project/experience_builder/issues/3485502))
 
-These checks are implemented in `experience_builder_block_alter()`.
+These checks are implemented in `\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent::checkRequirements()`.
 
 _Note:_ this list of criteria is not final, it will keep evolving _at least_ until a `1.0` release of XB.
 
diff --git a/experience_builder.module b/experience_builder.module
index d72a204458f8f89fce4c5566e8434d7b8e96ff0b..d749147f5a556ee62fda977fa07d7991d0b6ee8f 100644
--- a/experience_builder.module
+++ b/experience_builder.module
@@ -12,9 +12,7 @@ ini_set('assert.active', 1);
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Block\BlockManagerInterface;
-use Drupal\Core\Block\BlockPluginInterface;
 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
-use Drupal\Core\Block\MainContentBlockPluginInterface;
 use Drupal\Core\Entity\EntityPublishedInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
@@ -25,14 +23,11 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 use Drupal\experience_builder\Entity\AssetLibrary;
-use Drupal\experience_builder\Entity\Component;
 use Drupal\experience_builder\Entity\JavaScriptComponent;
 use Drupal\experience_builder\Form\FormIdPreRender;
 use Drupal\experience_builder\Entity\PageRegion;
 use Drupal\experience_builder\Plugin\ComponentPluginManager;
-use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
 use Drupal\experience_builder\Plugin\DisplayVariant\XbPageVariant;
-use Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent;
 use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem;
 use Drupal\experience_builder\PropExpressions\StructuredData\FieldPropExpression;
 use Drupal\experience_builder\PropExpressions\StructuredData\FieldTypeObjectPropsExpression;
@@ -209,88 +204,6 @@ function experience_builder_rebuild(): void {
   \Drupal::service(ComponentPluginManager::class)->getDefinitions();
 }
 
-/**
- * Implements hook_block_alter().
- */
-function experience_builder_block_alter(array &$definitions): void {
-  static $in_recursion = FALSE;
-  // Ensure we're not in infinite recursion.
-  if ($in_recursion) {
-    return;
-  }
-
-  // @todo Remove when minimum version is Drupal 11.1 following https://www.drupal.org/project/drupal/issues/3379725
-  $additional = version_compare(\Drupal::VERSION, '11.1', '>=') ? [] : [
-    'info' => '',
-    'status' => TRUE,
-    'view_mode' => '',
-    'context_mapping' => [],
-  ];
-
-  // @todo This only handles new & existing Component entities in best case scenario, but does not handle cases where BlockComponent based entities exist, but Block Plugin definition is missing, see https://www.drupal.org/project/experience_builder/issues/3484682
-  foreach ($definitions as $id => $definition) {
-    if ($id === 'broken') {
-      continue;
-    }
-
-    $component_id = 'block.' . str_replace(':', '.', $id);
-    $component = Component::load($component_id);
-    if ($component instanceof Component) {
-      // @todo Update Component entities with BlockComponent source plugin: https://www.drupal.org/project/experience_builder/issues/3484682
-      continue;
-    }
-    else {
-      $in_recursion = TRUE;
-      // @todo This is super ugly, decorate the block plugin manager so we can create instances directly?
-      // @todo is this a not going to become performance bottle neck on BlockPlugin heavy sites?
-      $block = \Drupal::service('plugin.manager.block')->createInstance($id);
-      assert($block instanceof BlockPluginInterface);
-      // The main content is rendered in a fixed position.
-      // @see \Drupal\experience_builder\Plugin\DisplayVariant\XbPageVariant::build()
-      if ($block instanceof MainContentBlockPluginInterface) {
-        continue;
-      }
-      $settings = $block->defaultConfiguration();
-      $data_definition = \Drupal::service('config.typed')->createFromNameAndData('block.settings.' . $id, $settings);
-      // We currently support only block plugins with no settings, or if they do
-      // have settings, they must be fully validatable.
-      $fullyValidatable = FALSE;
-      foreach ($data_definition->getConstraints() as $constraint) {
-        if ($constraint instanceof FullyValidatableConstraint) {
-          $fullyValidatable = TRUE;
-          break;
-        }
-      }
-      // @todo Remove the PageTitleBlock and SystemMessagesBlock special cases: make it fully validatable upstream in Drupal core. They are exempted here because of its crucial role in \Drupal\experience_builder\Entity\PageTemplate. Alternatively, this can be removed once XB requires Drupal 11.
-      // @todo Remove the LocalActionsBlock special case: it is necessary to be able to test BlockPluginInterface::access() support *and* it has the exact same trivial settings as the two crucial blocks above. Alternatively, this can be removed once XB requires Drupal 11.
-      if (!empty($settings) && !$fullyValidatable && $id !== 'page_title_block' && $id !== 'system_messages_block' && $id != 'local_actions_block') {
-        continue;
-      }
-      $component = Component::create([
-        'id' => $component_id,
-        'label' => (string) $definition['admin_label'],
-        'category' => (string) $definition['category'],
-        'source' => BlockComponent::SOURCE_PLUGIN_ID,
-        'provider' => $definition['provider'],
-        'settings' => [
-          'plugin_id' => $id,
-          // We are using strict config schema validation, so we need to provide valid default settings for each block.
-          'default_settings' => [
-              // @todo if we need ID here can we merge settings with the parent and drop plugin_id?
-            'id' => $id,
-            'label' => (string) $definition['admin_label'],
-            'label_display' => FALSE,
-            'provider' => $definition['provider'],
-          ] + $additional + $settings,
-        ],
-        'status' => TRUE,
-      ]);
-      $component->save();
-      $in_recursion = FALSE;
-    }
-  }
-}
-
 /**
  * Implements hook_config_schema_info_alter().
  */
diff --git a/experience_builder.services.yml b/experience_builder.services.yml
index 4c67d87f7644b5d8aea0259d5cfe729a22ea4097..77f7cfd22e388beaf2346992113f847d123b926c 100644
--- a/experience_builder.services.yml
+++ b/experience_builder.services.yml
@@ -31,6 +31,10 @@ services:
     decorates: Drupal\Core\Theme\ComponentPluginManager
     parent: Drupal\Core\Theme\ComponentPluginManager
     arguments: ['@entity_type.manager', '@Drupal\experience_builder\ComponentIncompatibilityReasonRepository']
+  Drupal\experience_builder\Plugin\BlockManager:
+    decorates: Drupal\Core\Block\BlockManagerInterface
+    parent: Drupal\Core\Block\BlockManagerInterface
+    arguments: ['@config.typed', '@Drupal\experience_builder\ComponentIncompatibilityReasonRepository']
   Drupal\experience_builder\ComponentSource\ComponentSourceManager:
     parent: default_plugin_manager
     public: true
diff --git a/phpstan.neon b/phpstan.neon
index 0e2e9ff7c02f2334d7cbb8f988ccbacc85d705ff..fda757ed67938cb96a5682fc3bd2db7f04cb350f 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -34,9 +34,11 @@ parameters:
       count: 1
       path: src/PropSource/StaticPropSource.php
     -
-      message: "#^Missing cache backend declaration for performance\\.$#"
-      count: 1
-      path: src/Plugin/ComponentPluginManager.php
+      messages:
+        - "#^Missing cache backend declaration for performance\\.$#"
+      paths:
+        - src/Plugin/BlockManager.php
+        - src/Plugin/ComponentPluginManager.php
     -
       message: "#^Cannot assign offset 'id' to string\\.$#"
       count: 1
diff --git a/src/ComponentDoesNotMeetRequirementsException.php b/src/ComponentDoesNotMeetRequirementsException.php
index 5d0887368e409dd3c7ea2c37bc6ee3dafcafc0f1..b1dda871de538834e5a82d116877a651326a748a 100644
--- a/src/ComponentDoesNotMeetRequirementsException.php
+++ b/src/ComponentDoesNotMeetRequirementsException.php
@@ -9,4 +9,16 @@ namespace Drupal\experience_builder;
  */
 final class ComponentDoesNotMeetRequirementsException extends \Exception {
 
+  public function __construct(
+    protected readonly array $messages,
+    int $code = 0,
+    ?\Throwable $previous = NULL,
+  ) {
+    parent::__construct(\implode("\n", $this->messages), $code, $previous);
+  }
+
+  public function getMessages(): array {
+    return $this->messages;
+  }
+
 }
diff --git a/src/ComponentIncompatibilityReasonRepository.php b/src/ComponentIncompatibilityReasonRepository.php
index 270557de721375a3dc97f387753abb8fdcbbd87f..9264e74da5a00e80dcf45d0e72a642a2e7fdeaae 100644
--- a/src/ComponentIncompatibilityReasonRepository.php
+++ b/src/ComponentIncompatibilityReasonRepository.php
@@ -23,9 +23,16 @@ final class ComponentIncompatibilityReasonRepository {
     $this->keyValue = $keyValueFactory->get('experience_builder:component:reasons');
   }
 
-  public function storeReason(string $source_plugin_id, string $identifier, string $reason): void {
+  /**
+   * @param string $source_plugin_id
+   * @param string $identifier
+   * @param array<int, string> $reasons
+   *
+   * @return void
+   */
+  public function storeReasons(string $source_plugin_id, string $identifier, array $reasons): void {
     $key = $this->generateKey($source_plugin_id, $identifier);
-    $this->keyValue->set($key, $reason);
+    $this->keyValue->set($key, $reasons);
   }
 
   public function removeReason(string $source_plugin_id, string $identifier): void {
diff --git a/src/ComponentMetadataRequirementsChecker.php b/src/ComponentMetadataRequirementsChecker.php
index 01abc5d31f4779f1149e951fd77facf1dcc9ed61..4a3837ab33cef8f94729bccdb86a47e6733f2753 100644
--- a/src/ComponentMetadataRequirementsChecker.php
+++ b/src/ComponentMetadataRequirementsChecker.php
@@ -28,15 +28,16 @@ final class ComponentMetadataRequirementsChecker {
    *   When the component does not meet requirements.
    */
   public static function check(string $component_id, ComponentMetadata $metadata, array $required_props): void {
+    $messages = [];
     // XB always requires schema, even for theme components.
     // @see \Drupal\Core\Theme\ComponentPluginManager::shouldEnforceSchemas()
     // @see \Drupal\Core\Theme\Component\ComponentMetadata::parseSchemaInfo()
     if ($metadata->schema === NULL) {
-      throw new ComponentDoesNotMeetRequirementsException('Component has no props schema');
+      throw new ComponentDoesNotMeetRequirementsException(['Component has no props schema']);
     }
 
     if ($metadata->group == 'Elements') {
-      throw new ComponentDoesNotMeetRequirementsException('Component uses the reserved "Elements" category');
+      $messages[] = 'Component uses the reserved "Elements" category';
     }
 
     $missing_examples = \array_filter(
@@ -44,9 +45,7 @@ final class ComponentMetadataRequirementsChecker {
       static fn (array $property) => empty($property['examples'])
     );
     if (\count($missing_examples) > 0) {
-      throw new ComponentDoesNotMeetRequirementsException(
-        \implode("\n", \array_map(static fn(string $prop) => \sprintf('Prop "%s" is required, but does not have example value', $prop), \array_keys($missing_examples)))
-      );
+      $messages += \array_map(static fn(string $prop) => \sprintf('Prop "%s" is required, but does not have example value', $prop), \array_keys($missing_examples));
     }
 
     $props_for_metadata = PropShape::getComponentPropsForMetadata($component_id, $metadata);
@@ -56,7 +55,7 @@ final class ComponentMetadataRequirementsChecker {
       }
       // Every prop must have a title.
       if (!isset($prop['title'])) {
-        throw new ComponentDoesNotMeetRequirementsException(\sprintf('Prop "%s" must have title', $prop_name));
+        $messages[] = \sprintf('Prop "%s" must have title', $prop_name);
       }
       // Every prop must have a StorablePropShape.
       $component_prop_expression = new ComponentPropExpression($component_id, $prop_name);
@@ -65,7 +64,10 @@ final class ComponentMetadataRequirementsChecker {
       if ($storable_prop_shape instanceof StorablePropShape) {
         continue;
       }
-      throw new ComponentDoesNotMeetRequirementsException(\sprintf('Experience Builder does not know of a field type/widget to allow populating the <code>%s</code> prop, with the shape <code>%s</code>.', $prop_name, json_encode($prop_shape->schema, JSON_UNESCAPED_SLASHES)));
+      $messages[] = \sprintf('Experience Builder does not know of a field type/widget to allow populating the <code>%s</code> prop, with the shape <code>%s</code>.', $prop_name, json_encode($prop_shape->schema, JSON_UNESCAPED_SLASHES));
+    }
+    if (!empty($messages)) {
+      throw new ComponentDoesNotMeetRequirementsException($messages);
     }
   }
 
diff --git a/src/Controller/ComponentStatusController.php b/src/Controller/ComponentStatusController.php
index ad40cc7c0da70fd30d427b459690e6fb319dc91b..9abc857755ae16163264eb24b97a5389b4e89e02 100644
--- a/src/Controller/ComponentStatusController.php
+++ b/src/Controller/ComponentStatusController.php
@@ -41,28 +41,27 @@ final class ComponentStatusController {
     $reasons = $this->reasonRepository->getReasons();
     $rows = [];
     $header = [
-      [
-        'data' => $this->t('Component'),
-      ],
-      [
-        'data' => $this->t('Status'),
-      ],
-      [
-        'data' => $this->t('Reason'),
-      ],
+      'id' => $this->t('Component'),
+      'status' => $this->t('Status'),
+      'reason' => $this->t('Reason'),
     ];
     foreach ($reasons as $source_reasons) {
-      foreach ($source_reasons as $component_id => $reason) {
+      foreach ($source_reasons as $component_id => $component_reasons) {
         $component_entity = Component::load($component_id);
         $status = $component_entity instanceof Component && !$component_entity->status() ? $this->t('Disabled') : $this->t('Incompatible');
-
-        $rows[] = [
-          'data' => [
-            $component_id,
-            $status,
-            Markup::create($reason),
-          ],
+        $items = [];
+        $component_reasons = is_string($component_reasons) ? [$component_reasons] : $component_reasons;
+        foreach ($component_reasons as $item) {
+          $items[] = Markup::create($item);
+        }
+        $row = [];
+        $row['id']['data'] = $component_id;
+        $row['status']['data'] = $status;
+        $row['reason']['data'] = [
+          '#theme' => 'item_list',
+          '#items' => $items,
         ];
+        $rows[] = $row;
       }
     }
 
@@ -93,7 +92,7 @@ final class ComponentStatusController {
     $source_plugin_id = $source->getPluginId();
     if ($op === 'disable') {
       $component->disable()->save();
-      $this->reasonRepository->storeReason($source_plugin_id, $component_id, 'Manually disabled');
+      $this->reasonRepository->storeReasons($source_plugin_id, $component_id, ['Manually disabled']);
     }
     elseif ($op === 'enable') {
       try {
@@ -106,7 +105,7 @@ final class ComponentStatusController {
           "%component" => $component_id,
           "%reason" => $e->getMessage(),
         ]));
-        $this->reasonRepository->storeReason($source_plugin_id, $component_id, $e->getMessage());
+        $this->reasonRepository->storeReasons($source_plugin_id, $component_id, $e->getMessages());
         return new RedirectResponse(Url::fromRoute('entity.component.collection')->toString());
       }
     }
diff --git a/src/Entity/PageRegion.php b/src/Entity/PageRegion.php
index 48ad84924b88eea850a664e0bb766f006760556d..bc0578fd5ac6809dadb4148a9ca19f76b0377600 100644
--- a/src/Entity/PageRegion.php
+++ b/src/Entity/PageRegion.php
@@ -235,7 +235,7 @@ final class PageRegion extends ConfigEntityBase {
       $component_id = BlockComponent::componentIdFromBlockPluginId($block->getPluginId());
       if (!Component::load($component_id)) {
         // This block isn't supported by XB.
-        // @see \experience_builder_block_alter().
+        // @see \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent::checkRequirements()
         continue;
       }
       $region_name = match ($block->getRegion()) {
diff --git a/src/EntityHandlers/JavascriptComponentStorage.php b/src/EntityHandlers/JavascriptComponentStorage.php
index 81c5b7d8d989ada47bf3c1aa283db03e7626d3e1..15fb15e76cdfedf02bc32866efec5ca30bdb736d 100644
--- a/src/EntityHandlers/JavascriptComponentStorage.php
+++ b/src/EntityHandlers/JavascriptComponentStorage.php
@@ -132,7 +132,7 @@ final class JavascriptComponentStorage extends XbAssetStorage {
   }
 
   private function handleComponentDoesNotMeetRequirementsException(string $component_id, ComponentDoesNotMeetRequirementsException $e): void {
-    $this->componentIncompatibilityReasonRepository->storeReason(JsComponent::SOURCE_PLUGIN_ID, $component_id, $e->getMessage());
+    $this->componentIncompatibilityReasonRepository->storeReasons(JsComponent::SOURCE_PLUGIN_ID, $component_id, $e->getMessages());
   }
 
 }
diff --git a/src/Plugin/BlockManager.php b/src/Plugin/BlockManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..35ca65163d1151912cc741abf1616f3ce962e3b6
--- /dev/null
+++ b/src/Plugin/BlockManager.php
@@ -0,0 +1,115 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\experience_builder\Plugin;
+
+use Drupal\Core\Block\BlockManager as CoreBlockManager;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Block\MainContentBlockPluginInterface;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\experience_builder\ComponentDoesNotMeetRequirementsException;
+use Drupal\experience_builder\ComponentIncompatibilityReasonRepository;
+use Drupal\experience_builder\Entity\Component;
+use Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Decorator that auto-creates/updates an Experience Builder Component entity per Block plugin.
+ *
+ * @see \Drupal\experience_builder\Entity\Component
+ * @see docs/components.md#3.2
+ */
+final class BlockManager extends CoreBlockManager {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(
+    \Traversable $namespaces,
+    CacheBackendInterface $cache_backend,
+    ModuleHandlerInterface $module_handler,
+    LoggerInterface $logger,
+    protected readonly TypedConfigManagerInterface $configTyped,
+    private readonly ComponentIncompatibilityReasonRepository $reasonRepository,
+  ) {
+    parent::__construct($namespaces, $cache_backend, $module_handler, $logger);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setCachedDefinitions($definitions): array {
+    parent::setCachedDefinitions($definitions);
+
+    // Do not auto-create/update XB configuration when syncing config/deploying.
+    // @todo Introduce a "XB development mode" similar to Twig's: https://www.drupal.org/node/3359728
+    // @phpstan-ignore-next-line
+    if (\Drupal::isConfigSyncing()) {
+      return $definitions;
+    }
+
+    // @todo Remove this in Drupal 11 following https://www.drupal.org/project/drupal/issues/3379725
+    [$version] = explode('.', \Drupal::VERSION);
+    $additional = $version > 10 ? [] : [
+      'info' => '',
+      'status' => TRUE,
+      'view_mode' => '',
+      'context_mapping' => [],
+    ];
+
+    foreach ($definitions as $id => $definition) {
+      if ($id === 'broken') {
+        continue;
+      }
+
+      $component_id = 'block.' . str_replace(':', '.', $id);
+      $component = Component::load($component_id);
+      if ($component instanceof Component) {
+        // @todo Update Component entities with BlockComponent source plugin: https://www.drupal.org/project/experience_builder/issues/3484682
+        continue;
+      }
+
+      // @todo is this a not going to become performance bottle neck on BlockPlugin heavy sites?
+      $block = $this->createInstance($id);
+      assert($block instanceof BlockPluginInterface);
+      // The main content is rendered in a fixed position.
+      // @see \Drupal\experience_builder\Plugin\DisplayVariant\XbPageVariant::build()
+      if ($block instanceof MainContentBlockPluginInterface) {
+        continue;
+      }
+      $settings = $block->defaultConfiguration();
+      $component = Component::create([
+        'id' => $component_id,
+        'label' => (string) $definition['admin_label'],
+        'category' => (string) $definition['category'],
+        'source' => BlockComponent::SOURCE_PLUGIN_ID,
+        'provider' => $definition['provider'],
+        'settings' => [
+          'plugin_id' => $id,
+          // We are using strict config schema validation, so we need to provide valid default settings for each block.
+          'default_settings' => [
+            // @todo if we need ID here can we merge settings with the parent and drop plugin_id?
+            'id' => $id,
+            'label' => (string) $definition['admin_label'],
+            'label_display' => FALSE,
+            'provider' => $definition['provider'],
+          ] + $additional + $settings,
+        ],
+        'status' => TRUE,
+      ]);
+      try {
+        $component->getComponentSource()->checkRequirements();
+        $component->save();
+      }
+      catch (ComponentDoesNotMeetRequirementsException $e) {
+        $this->reasonRepository->storeReasons($block->getPluginId(), $component_id, $e->getMessages());
+      }
+    }
+
+    return $definitions;
+  }
+
+}
diff --git a/src/Plugin/ComponentPluginManager.php b/src/Plugin/ComponentPluginManager.php
index 37c21b82b9ed25021363689b048aaa6683be6b0d..ee8d1cefa8c8fe080c4249cb33f271ce520a309d 100644
--- a/src/Plugin/ComponentPluginManager.php
+++ b/src/Plugin/ComponentPluginManager.php
@@ -84,7 +84,7 @@ class ComponentPluginManager extends CoreComponentPluginManager implements Categ
         $component_plugin = $this->createInstance($machine_name);
         $component = SingleDirectoryComponent::updateConfigEntity($component_plugin);
         if (isset($component_plugin->metadata->status) && $component_plugin->metadata->status === 'obsolete') {
-          $reasons[$component_id] = 'Component has "obsolete" status';
+          $reasons[$component_id][] = 'Component has "obsolete" status';
           $component->disable();
         }
       }
@@ -95,7 +95,7 @@ class ComponentPluginManager extends CoreComponentPluginManager implements Categ
           $component = SingleDirectoryComponent::createConfigEntity($component_plugin);
         }
         catch (ComponentDoesNotMeetRequirementsException $e) {
-          $reasons[$component_id] = $e->getMessage();
+          $reasons[$component_id] = $e->getMessages();
           continue;
         }
       }
diff --git a/src/Plugin/DisplayVariant/XbPageVariant.php b/src/Plugin/DisplayVariant/XbPageVariant.php
index 5805f5ae9ce1567d08aab7902e593a3682c29d0a..89e2a4f9df3803f1c1fe4967ac0717ddde434ccb 100644
--- a/src/Plugin/DisplayVariant/XbPageVariant.php
+++ b/src/Plugin/DisplayVariant/XbPageVariant.php
@@ -40,7 +40,7 @@ use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem;
  * Finally, MainContentBlockPluginInterface implementations are prevented from
  * being made available as XB Components.
  *
- * @see experience_builder_block_alter()
+ * @see \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent::checkRequirements()
  *
  * @see docs/components.md
  * @see \Drupal\Core\Render\Element\Page
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
index 84caddb8c6d0a9ec8766d21f593d080e8ceb0d9f..4fbac989cf6c152bef5d8f9b995f8d9dd26f5e48 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/BlockComponent.php
@@ -7,6 +7,7 @@ namespace Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource;
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Block\BlockManagerInterface;
 use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Block\MainContentBlockPluginInterface;
 use Drupal\Core\Block\MessagesBlockPluginInterface;
 use Drupal\Core\Block\TitleBlockPluginInterface;
 use Drupal\Core\Config\TypedConfigManagerInterface;
@@ -21,7 +22,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
 use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
 use Drupal\experience_builder\Attribute\ComponentSource;
+use Drupal\experience_builder\ComponentDoesNotMeetRequirementsException;
 use Drupal\experience_builder\ComponentSource\ComponentSourceBase;
 use Drupal\experience_builder\Entity\Component;
 use Drupal\experience_builder\Entity\Component as ComponentEntity;
@@ -388,7 +391,29 @@ final class BlockComponent extends ComponentSourceBase implements ContainerFacto
    * {@inheritdoc}
    */
   public function checkRequirements(): void {
-    // @todo Move logic from experience_builder_block_alter here in https://www.drupal.org/project/experience_builder/issues/3491032
+    $block = $this->getBlockPlugin();
+    // The main content is rendered in a fixed position.
+    // @see \Drupal\experience_builder\Plugin\DisplayVariant\XbPageVariant::build()
+    if ($block instanceof MainContentBlockPluginInterface) {
+      return;
+    }
+    $settings = $block->defaultConfiguration();
+    $data_definition = $this->typedConfigManager->createFromNameAndData('block.settings.' . $block->getPluginId(), $settings);
+    // We currently support only block plugins with no settings, or if they do
+    // have settings, they must be fully validatable.
+    $fullyValidatable = FALSE;
+    foreach ($data_definition->getConstraints() as $constraint) {
+      if ($constraint instanceof FullyValidatableConstraint) {
+        $fullyValidatable = TRUE;
+        break;
+      }
+    }
+
+    // @todo Remove the PageTitleBlock and SystemMessagesBlock special cases: make it fully validatable upstream in Drupal core. They are exempted here because of its crucial role in \Drupal\experience_builder\Entity\PageTemplate. Alternatively, this can be removed once XB requires Drupal 11.
+    // @todo Remove the LocalActionsBlock special case: it is necessary to be able to test BlockPluginInterface::access() support *and* it has the exact same trivial settings as the two crucial blocks above. Alternatively, this can be removed once XB requires Drupal 11.
+    if (!empty($settings) && !$fullyValidatable && !in_array($block->getPluginId(), ['page_title_block', 'system_messages_block', 'local_actions_block'])) {
+      throw new ComponentDoesNotMeetRequirementsException(['Block plugin settings must be fully validatable']);
+    }
   }
 
 }
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
index 7e3c386fbaa2b6e0b1f56e94b36315f9bb524574..aec6006cc3f908da681ca873c22116c07aff74b2 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php
@@ -233,7 +233,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase
       $ephemeral_sdc_component = self::buildEphemeralSdcPluginInstance($js_component);
     }
     catch (InvalidComponentException $e) {
-      throw new ComponentDoesNotMeetRequirementsException($e->getMessage());
+      throw new ComponentDoesNotMeetRequirementsException([$e->getMessage()]);
     }
     ComponentMetadataRequirementsChecker::check((string) $js_component->id(), $ephemeral_sdc_component->metadata, $js_component->getRequiredProps());
     $props = self::getPropsForComponentPlugin($ephemeral_sdc_component);
@@ -274,7 +274,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase
       $ephemeral_sdc_component = self::buildEphemeralSdcPluginInstance($js_component);
     }
     catch (InvalidComponentException $e) {
-      throw new ComponentDoesNotMeetRequirementsException($e->getMessage());
+      throw new ComponentDoesNotMeetRequirementsException([$e->getMessage()]);
     }
     ComponentMetadataRequirementsChecker::check((string) $js_component->id(), $ephemeral_sdc_component->metadata, $js_component->getRequiredProps());
     $settings['prop_field_definitions'] = self::getPropsForComponentPlugin($ephemeral_sdc_component);
@@ -304,7 +304,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase
       $ephemeral_sdc_component = self::buildEphemeralSdcPluginInstance($js_component);
     }
     catch (InvalidComponentException $e) {
-      throw new ComponentDoesNotMeetRequirementsException($e->getMessage());
+      throw new ComponentDoesNotMeetRequirementsException([$e->getMessage()]);
     }
     ComponentMetadataRequirementsChecker::check((string) $js_component->id(), $ephemeral_sdc_component->metadata, $js_component->getRequiredProps());
   }
diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
index 7155c1bc7e1400c1f920fcde3f78f01be5bc6e75..2780ee5097c45c528762cb8a663ce96301cc3f4d 100644
--- a/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
+++ b/src/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponent.php
@@ -280,7 +280,7 @@ final class SingleDirectoryComponent extends GeneratedFieldExplicitInputUxCompon
     \assert(\is_array($definition));
 
     if (isset($definition['status']) && $definition['status'] === 'obsolete') {
-      throw new ComponentDoesNotMeetRequirementsException('Component has "obsolete" status');
+      throw new ComponentDoesNotMeetRequirementsException(['Component has "obsolete" status']);
     }
     // Special case exception for 'all-props' SDC.
     // (This is used to develop support for more prop shapes.)
diff --git a/src/Plugin/Validation/Constraint/JsComponentHasValidAndSupportedSdcMetadataConstraintValidator.php b/src/Plugin/Validation/Constraint/JsComponentHasValidAndSupportedSdcMetadataConstraintValidator.php
index 8e0a715d2f4fe81c40ba5f1efb175ccd84fb0689..cc29399c062ef34df8dcc825c7cb537c640a062b 100644
--- a/src/Plugin/Validation/Constraint/JsComponentHasValidAndSupportedSdcMetadataConstraintValidator.php
+++ b/src/Plugin/Validation/Constraint/JsComponentHasValidAndSupportedSdcMetadataConstraintValidator.php
@@ -73,7 +73,9 @@ final class JsComponentHasValidAndSupportedSdcMetadataConstraintValidator extend
       JsComponent::createConfigEntity($data);
     }
     catch (ComponentDoesNotMeetRequirementsException $e) {
-      $this->context->addViolation($e->getMessage());
+      foreach ($e->getMessages() as $message) {
+        $this->context->addViolation($message);
+      }
     }
   }
 
diff --git a/tests/src/Functional/XbConfigEntityHttpApiTest.php b/tests/src/Functional/XbConfigEntityHttpApiTest.php
index 79fa474331247c6964fbc504f187592897d966ab..34a691a91380a82e17c291143b0899c06f6bad52 100644
--- a/tests/src/Functional/XbConfigEntityHttpApiTest.php
+++ b/tests/src/Functional/XbConfigEntityHttpApiTest.php
@@ -689,6 +689,10 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase {
           'detail' => 'Prop "integer" is required, but does not have example value',
           'source' => ['pointer' => ''],
         ],
+        [
+          'detail' => 'Prop "string" must have title',
+          'source' => ['pointer' => ''],
+        ],
         [
           'detail' => "'title' is a required key.",
           'source' => ['pointer' => 'props.string'],
diff --git a/tests/src/Kernel/ComponentIncompatibilityReasonRepositoryTest.php b/tests/src/Kernel/ComponentIncompatibilityReasonRepositoryTest.php
index cc6c85d8a248bfe25a5ec6b4374676dc79f8334f..e9c8dfe280c2c1be8c02823a809d82d675e07052 100644
--- a/tests/src/Kernel/ComponentIncompatibilityReasonRepositoryTest.php
+++ b/tests/src/Kernel/ComponentIncompatibilityReasonRepositoryTest.php
@@ -27,34 +27,40 @@ final class ComponentIncompatibilityReasonRepositoryTest extends KernelTestBase
   public function testRepository(): void {
     $repository = $this->container->get(ComponentIncompatibilityReasonRepository::class);
     \assert($repository instanceof ComponentIncompatibilityReasonRepository);
-    $repository->storeReason('sketches', 'house', 'Missing door');
-    $repository->storeReason('sketches', 'dog', 'Missing tail');
-    $repository->storeReason('petra', 'dragon', 'Climate apocalypse');
+    $repository->storeReasons('sketches', 'house', ['Missing door']);
+    $repository->storeReasons('sketches', 'dog', ['Missing tail']);
+    $repository->storeReasons('petra', 'dragon', ['Climate apocalypse', 'Large and scaly']);
     self::assertEquals([
       'sketches' => [
-        'house' => 'Missing door',
-        'dog' => 'Missing tail',
+        'house' => ['Missing door'],
+        'dog' => ['Missing tail'],
       ],
       'petra' => [
-        'dragon' => 'Climate apocalypse',
+        'dragon' => [
+          'Climate apocalypse',
+          'Large and scaly',
+        ],
       ],
     ], $repository->getReasons());
     $repository->removeReason('sketches', 'house');
     self::assertEquals([
       'sketches' => [
-        'dog' => 'Missing tail',
+        'dog' => ['Missing tail'],
       ],
       'petra' => [
-        'dragon' => 'Climate apocalypse',
+        'dragon' => [
+          'Climate apocalypse',
+          'Large and scaly',
+        ],
       ],
     ], $repository->getReasons());
-    $repository->updateReasons('petra', ['converge' => 'Gray snakes slither across country']);
+    $repository->updateReasons('petra', ['converge' => ['Gray snakes slither across country']]);
     self::assertEquals([
       'sketches' => [
-        'dog' => 'Missing tail',
+        'dog' => ['Missing tail'],
       ],
       'petra' => [
-        'converge' => 'Gray snakes slither across country',
+        'converge' => ['Gray snakes slither across country'],
       ],
     ], $repository->getReasons());
   }
diff --git a/tests/src/Kernel/Config/ComponentTest.php b/tests/src/Kernel/Config/ComponentTest.php
index 06ec7607176bbd0a8257e7a3cea96e5c5efe28b8..4401714d8ed5ec184637a92c3948ac6e5242f990 100644
--- a/tests/src/Kernel/Config/ComponentTest.php
+++ b/tests/src/Kernel/Config/ComponentTest.php
@@ -267,7 +267,7 @@ class ComponentTest extends KernelTestBase {
       $plugin_id = str_replace('.', ':', $plugin_id);
       $expected_plugins[$type][] = $plugin_id;
       $this->assertSame($component_entity['compatible'], Component::load($component_id) instanceof Component, $plugin_id . ' and modules: ' . implode(', ', $modules));
-      $this->assertSame($component_entity['reason'] ?? NULL, isset($reasons[$component_id]) ? (string) $reasons[$component_id] : NULL, $plugin_id);
+      $this->assertSame($component_entity['reasons'] ?? NULL, isset($reasons[$component_id]) && !empty($reasons[$component_id]) ? $reasons[$component_id] : NULL, $plugin_id);
     }
 
     $this->assertEqualsCanonicalizing($expected_plugins['sdc'], array_keys($this->componentPluginManager->getDefinitions()));
@@ -277,7 +277,7 @@ class ComponentTest extends KernelTestBase {
         $expected_plugins['block'][] = 'system_clear_cache_block';
       }
       $this->assertEqualsCanonicalizing($expected_plugins['block'], array_diff($all_installed_block_plugin_ids, [
-        // @see \experience_builder_block_alter()
+        // @see \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent::checkRequirements()
         'system_main_block',
       ]));
     }
@@ -289,7 +289,7 @@ class ComponentTest extends KernelTestBase {
     $defaults = [
       'sdc.experience_builder.obsolete' => [
         'compatible' => FALSE,
-        'reason' => 'Component has "obsolete" status',
+        'reasons' => ['Component has "obsolete" status'],
       ],
       'sdc.experience_builder.druplicon' => [
         'compatible' => TRUE,
@@ -314,7 +314,7 @@ class ComponentTest extends KernelTestBase {
       ],
       'sdc.experience_builder.video' => [
         'compatible' => FALSE,
-        'reason' => 'Experience Builder does not know of a field type/widget to allow populating the <code>src</code> prop, with the shape <code>{"type":"string","format":"uri","pattern":"\\\.(mp4|webm)(\\\?.*)?(#.*)?$"}</code>.',
+        'reasons' => ['Experience Builder does not know of a field type/widget to allow populating the <code>src</code> prop, with the shape <code>{"type":"string","format":"uri","pattern":"\\\.(mp4|webm)(\\\?.*)?(#.*)?$"}</code>.'],
       ],
       'sdc.experience_builder.shoe_tab_panel' => [
         'compatible' => TRUE,
@@ -324,7 +324,7 @@ class ComponentTest extends KernelTestBase {
       ],
       'sdc.experience_builder.shoe_button' => [
         'compatible' => FALSE,
-        'reason' => 'Experience Builder does not know of a field type/widget to allow populating the <code>icon</code> prop, with the shape <code>{"type":"object","$ref":"json-schema-definitions://experience_builder.module/shoe-icon"}</code>.',
+        'reasons' => ['Experience Builder does not know of a field type/widget to allow populating the <code>icon</code> prop, with the shape <code>{"type":"object","$ref":"json-schema-definitions://experience_builder.module/shoe-icon"}</code>.'],
       ],
       'sdc.experience_builder.shoe_icon' => [
         'compatible' => TRUE,
@@ -337,7 +337,10 @@ class ComponentTest extends KernelTestBase {
       ],
       'sdc.experience_builder.shoe_details' => [
         'compatible' => FALSE,
-        'reason' => 'Experience Builder does not know of a field type/widget to allow populating the <code>expand_icon</code> prop, with the shape <code>{"type":"object","$ref":"json-schema-definitions://experience_builder.module/shoe-icon"}</code>.',
+        'reasons' => [
+          'Experience Builder does not know of a field type/widget to allow populating the <code>expand_icon</code> prop, with the shape <code>{"type":"object","$ref":"json-schema-definitions://experience_builder.module/shoe-icon"}</code>.',
+          'Experience Builder does not know of a field type/widget to allow populating the <code>collapse_icon</code> prop, with the shape <code>{"type":"object","$ref":"json-schema-definitions://experience_builder.module/shoe-icon"}</code>.',
+        ],
       ],
       'sdc.experience_builder.my-hero' => [
         'compatible' => TRUE,
@@ -347,7 +350,7 @@ class ComponentTest extends KernelTestBase {
       ],
       'sdc.sdc_test.array-to-object' => [
         'compatible' => FALSE,
-        'reason' => 'Experience Builder does not know of a field type/widget to allow populating the <code>testProp</code> prop, with the shape <code>{"type":"object"}</code>.',
+        'reasons' => ['Experience Builder does not know of a field type/widget to allow populating the <code>testProp</code> prop, with the shape <code>{"type":"object"}</code>.'],
       ],
       'sdc.sdc_test.my-button' => [
         'compatible' => TRUE,
@@ -385,7 +388,7 @@ class ComponentTest extends KernelTestBase {
         ],
         'sdc.xb_test_sdc.image-required-without-example' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "image" is required, but does not have example value',
+          'reasons' => ['Prop "image" is required, but does not have example value'],
         ],
         'sdc.xb_test_sdc.props-no-slots' => [
           'compatible' => TRUE,
@@ -395,11 +398,11 @@ class ComponentTest extends KernelTestBase {
         ],
         'sdc.xb_test_sdc.props-no-title' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "heading" must have title',
+          'reasons' => ['Prop "heading" must have title'],
         ],
         'sdc.xb_test_sdc.props-no-examples' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "heading" is required, but does not have example value',
+          'reasons' => ['Prop "heading" is required, but does not have example value'],
         ],
       ],
       'classes' => [
@@ -424,7 +427,7 @@ class ComponentTest extends KernelTestBase {
         ],
         'sdc.xb_test_sdc.image-required-without-example' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "image" is required, but does not have example value',
+          'reasons' => ['Prop "image" is required, but does not have example value'],
         ],
         'sdc.xb_test_sdc.props-no-slots' => [
           'compatible' => TRUE,
@@ -434,11 +437,11 @@ class ComponentTest extends KernelTestBase {
         ],
         'sdc.xb_test_sdc.props-no-title' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "heading" must have title',
+          'reasons' => ['Prop "heading" must have title'],
         ],
         'sdc.xb_test_sdc.props-no-examples' => [
           'compatible' => FALSE,
-          'reason' => 'Prop "heading" is required, but does not have example value',
+          'reasons' => ['Prop "heading" is required, but does not have example value'],
         ],
       ],
       'classes' => [