diff --git a/.ddev/commands/host/rebuild b/.ddev/commands/web/rebuild
similarity index 57%
rename from .ddev/commands/host/rebuild
rename to .ddev/commands/web/rebuild
index 50965b14ae3cf2cdb8015077cc668cbe5c1d9e61..b5eeeea215d560beb4cd7ca52208985482935e64 100755
--- a/.ddev/commands/host/rebuild
+++ b/.ddev/commands/web/rebuild
@@ -4,6 +4,10 @@
 ## Usage: rebuild
 ## Example: "ddev rebuild"
 
-ddev drush sql:drop --yes
+drush sql:drop --yes
 rm -f composer.lock patches.lock.json
-ddev restart
+
+generate-composer-json > composer.json
+composer install
+composer patches-relock
+composer patches-repatch
diff --git a/recipes/drupal_cms_events/composer.json b/recipes/drupal_cms_events/composer.json
index 5c81aeef223fbace9d65ee5505263662a794bb88..3e58bb6c566f0b3e3997a95c749342d52fc61af8 100644
--- a/recipes/drupal_cms_events/composer.json
+++ b/recipes/drupal_cms_events/composer.json
@@ -12,7 +12,7 @@
     "drupal/drupal_cms_privacy_basic": "*",
     "drupal/geocoder": "^4.10",
     "drupal/geofield": "^1.47",
-    "drupal/leaflet": "^10.2.25",
+    "drupal/leaflet": "^10.2.33",
     "drupal/smart_date": "^4.2.1",
     "geocoder-php/nominatim-provider": "^5.7"
   }
diff --git a/recipes/drupal_cms_starter/config/eca.eca.init_search.yml b/recipes/drupal_cms_starter/config/eca.eca.init_search.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8a033660353c46b6801d8da517936f986ca54a8a
--- /dev/null
+++ b/recipes/drupal_cms_starter/config/eca.eca.init_search.yml
@@ -0,0 +1,62 @@
+# By resetting the last cron run time, this forces the `content` search index
+# to be rebuilt when it is created, because the next page request will run
+# cron via the automated_cron module. This can be removed when there is a
+# config action to rebuild a search index, or some other way for a recipe to
+# trigger a cron run.
+langcode: en
+status: true
+dependencies:
+  module:
+    - eca_base
+    - eca_config
+id: init_search
+modeller: fallback
+label: 'Initialize search index'
+version: 1.0.0
+weight: 0
+events:
+  Event_write_config:
+    plugin: 'config:save'
+    label: 'Write config'
+    configuration: {  }
+    successors:
+      -
+        id: Gateway_and_1
+        condition: Flow_is_search_index
+conditions:
+  Flow_is_search_index:
+    plugin: eca_scalar
+    configuration:
+      negate: false
+      case: false
+      left: '[config_name]'
+      right: search_api.index.content
+      operator: equal
+      type: value
+  Flow_is_new:
+    plugin: eca_scalar
+    configuration:
+      case: false
+      left: '[config_original:name]'
+      right: '[config:name]'
+      operator: equal
+      type: value
+      negate: true
+gateways:
+  Gateway_and_1:
+    type: 0
+    successors:
+      -
+        id: Activity_reset_last_cron_run
+        condition: Flow_is_new
+actions:
+  Activity_reset_last_cron_run:
+    plugin: eca_keyvaluestore_write
+    label: 'Reset last cron run'
+    configuration:
+      collection: state
+      key: system.cron_last
+      value: '0'
+      use_yaml: false
+      ifnotexists: false
+    successors: {  }
diff --git a/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php b/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
index a29988ba4f4aa6e22d377e83ab603826f5113733..1d8509705a2361c94767b1a0407e06af38ed42ec 100644
--- a/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
+++ b/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
@@ -6,6 +6,7 @@ namespace Drupal\Tests\drupal_cms_starter\Functional;
 
 use Composer\InstalledVersions;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\State\StateInterface;
 use Drupal\Tests\BrowserTestBase;
 use Symfony\Component\Process\PhpExecutableFinder;
 use Symfony\Component\Process\Process;
@@ -112,9 +113,54 @@ class ComponentValidationTest extends BrowserTestBase {
         // Pages should have the expected path aliases.
         $assert_session->addressMatches('/\/test-page$/');
       }
+
+      $this->drupalCreateNode([
+        'type' => $node_type,
+        'title' => "Search for this $node_type",
+        'moderation_state' => 'published',
+      ]);
     }
     $this->drupalLogout();
 
+    // If we apply the search recipe, the content we just created in the loop
+    // above should all be searchable.
+    $dir = InstalledVersions::getInstallPath('drupal/drupal_cms_search');
+    $this->applyRecipe($dir);
+
+    // The creation of the search index should have reset the last cron run time
+    // to zero.
+    /** @var \Drupal\Core\State\StateInterface $state */
+    $state = $this->container->get(StateInterface::class);
+    $last_cron_run = $state->get('system.cron_last');
+    $this->assertSame('0', $last_cron_run);
+    $last_cron_run = intval($last_cron_run);
+
+    // Thanks to automated_cron, this request will trigger a cron run.
+    $this->drupalGet('/search');
+
+    // The cron work may outlive the HTTP request, so wait for it to finish.
+    $seconds_waited = 0;
+    while ($last_cron_run === 0) {
+      $state->resetCache();
+      $last_cron_run = (int) $state->get('system.cron_last');
+
+      // We've given it a whole minute; it should be done by now.
+      if (++$seconds_waited === 60) {
+        break;
+      }
+      else {
+        sleep(1);
+      }
+    }
+    $this->assertGreaterThan(0, $last_cron_run);
+
+    // The content we created should now be searchable.
+    foreach ($node_types as $node_type) {
+      $page->fillField('Search keywords', $node_type);
+      $page->pressButton('Find');
+      $assert_session->linkExists("Search for this $node_type");
+    }
+
     // If you have permission to administer modules, you should see a dedicated
     // tab to browse recipes.
     $account = $this->drupalCreateUser(['administer modules']);