diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0f0a89fce7596989674edb4daaefe91e47db37f6..1a9399440e666aacdabe7e5794e6233234cdd2a6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -76,6 +76,8 @@ default:
     URL: 'git@git.drupal.org:project/drupal_cms_events.git'
   - DIR: recipes/drupal_cms_forms
     URL: 'git@git.drupal.org:project/drupal_cms_forms.git'
+  - DIR: recipes/drupal_cms_google_analytics
+    URL: 'git@git.drupal.org:project/drupal_cms_google_analytics.git'
   - DIR: recipes/drupal_cms_image
     URL: 'git@git.drupal.org:project/drupal_cms_image.git'
   - DIR: recipes/drupal_cms_news
@@ -159,6 +161,11 @@ run PHP tests:
   variables:
     # Skip performance tests by default.
     PHPUNIT_OPTIONS: '--exclude-group=OpenTelemetry $CI_PROJECT_DIR/$DIR'
+  rules:
+    # Don't test the `drupal_cms_analytics` recipe, because it is a metapackage.
+    - if: $DIR =~ /\/drupal_cms_analytics$/
+      when: never
+    - when: on_success
 
 test performance:
   extends: .phpunit-base
diff --git a/CODEOWNERS b/CODEOWNERS
index 61aac1d97fb8a0c9cae813cc783044ac987cb5da..59aea3e06dd55f198dc1e67ebf2d4df17c5d1993 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,6 +1,10 @@
 # The current committers own anything that's not explicitly assigned.
 * @phenaproxima @tim.plunkett @thejimbirch
 
+[analytics track] @dharizza
+/recipes/drupal_cms_analytics/
+/recipes/drupal_cms_google_analytics/
+
 [base recipes] @pameeela
 /recipes/drupal_cms_admin_theme/
 /recipes/drupal_cms_content_type_base/
diff --git a/dev.composer.json b/dev.composer.json
index d82c58dac77481b605f263c4c37e0db875a194d5..132d23a033a333b8e49e61e1daa014f1004054ac 100644
--- a/dev.composer.json
+++ b/dev.composer.json
@@ -48,6 +48,10 @@
             "type": "path",
             "url": "recipes/drupal_cms_forms"
         },
+        "google_analytics": {
+            "type": "path",
+            "url": "recipes/drupal_cms_google_analytics"
+        },
         "image": {
             "type": "path",
             "url": "recipes/drupal_cms_image"
diff --git a/recipes/drupal_cms_analytics/composer.json b/recipes/drupal_cms_analytics/composer.json
index 5aa34dd0c2bd9302be0b911f8eca3c5710dfd576..c3efee2a694ab73fabf2fcb2dc5640daec63609c 100644
--- a/recipes/drupal_cms_analytics/composer.json
+++ b/recipes/drupal_cms_analytics/composer.json
@@ -1,10 +1,8 @@
 {
     "name": "drupal/drupal_cms_analytics",
-    "type": "drupal-recipe",
-    "description": "Adds tracking of website traffic using Google Analytics and Google Tag Manager. Requires a Google Tag Manager ID.",
+    "type": "metapackage",
+    "description": "Adds recipes for tracking website traffic.",
     "require": {
-        "drupal/core": ">=10.4",
-        "drupal/google_tag": "^2.0.7",
-        "drupal/drupal_cms_privacy_basic": "*"
+        "drupal/drupal_cms_google_analytics": "*"
     }
 }
diff --git a/recipes/drupal_cms_google_analytics/composer.json b/recipes/drupal_cms_google_analytics/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..c242c2ddb35b7300046a0bf965f360c9d69ecda6
--- /dev/null
+++ b/recipes/drupal_cms_google_analytics/composer.json
@@ -0,0 +1,10 @@
+{
+    "name": "drupal/drupal_cms_google_analytics",
+    "type": "drupal-recipe",
+    "description": "Adds tracking of website traffic using Google Analytics and Google Tag Manager. Requires a Google Tag Manager ID.",
+    "require": {
+        "drupal/core": ">=10.4",
+        "drupal/google_tag": "^2.0.7",
+        "drupal/drupal_cms_privacy_basic": "*"
+    }
+}
diff --git a/recipes/drupal_cms_analytics/config/google_tag.container.drupal.yml b/recipes/drupal_cms_google_analytics/config/google_tag.container.drupal.yml
similarity index 100%
rename from recipes/drupal_cms_analytics/config/google_tag.container.drupal.yml
rename to recipes/drupal_cms_google_analytics/config/google_tag.container.drupal.yml
diff --git a/recipes/drupal_cms_analytics/recipe.yml b/recipes/drupal_cms_google_analytics/recipe.yml
similarity index 98%
rename from recipes/drupal_cms_analytics/recipe.yml
rename to recipes/drupal_cms_google_analytics/recipe.yml
index 86a225f046d1875640af1bae970c1536aa97e770..2fd7926334891322b91e0be1abb606e9cc282433 100644
--- a/recipes/drupal_cms_analytics/recipe.yml
+++ b/recipes/drupal_cms_google_analytics/recipe.yml
@@ -1,4 +1,4 @@
-name: Analytics
+name: Google Analytics
 type: Drupal CMS
 description: Adds tracking of website traffic using Google Analytics and Google Tag Manager. Requires a Google Tag Manager ID.
 recipes:
diff --git a/recipes/drupal_cms_analytics/tests/src/Functional/ComponentValidationTest.php b/recipes/drupal_cms_google_analytics/tests/src/Functional/ComponentValidationTest.php
similarity index 85%
rename from recipes/drupal_cms_analytics/tests/src/Functional/ComponentValidationTest.php
rename to recipes/drupal_cms_google_analytics/tests/src/Functional/ComponentValidationTest.php
index d8c1bcc693759ae1414bdb0eb321efa3cd820350..8f287e24f68995f0569c473a2722ddee38d4bdca 100644
--- a/recipes/drupal_cms_analytics/tests/src/Functional/ComponentValidationTest.php
+++ b/recipes/drupal_cms_google_analytics/tests/src/Functional/ComponentValidationTest.php
@@ -2,13 +2,13 @@
 
 declare(strict_types=1);
 
-namespace Drupal\Tests\drupal_cms_analytics\Functional;
+namespace Drupal\Tests\drupal_cms_google_analytics\Functional;
 
 use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
 use Drupal\Tests\BrowserTestBase;
 
 /**
- * @group drupal_cms_analytics
+ * @group drupal_cms_google_analytics
  */
 class ComponentValidationTest extends BrowserTestBase {
 
@@ -24,7 +24,7 @@ class ComponentValidationTest extends BrowserTestBase {
 
     // This recipe always requires a valid-looking Google Tag ID.
     $options = [
-      '--input=drupal_cms_analytics.property_id=GTM-123456',
+      '--input=drupal_cms_google_analytics.property_id=GTM-123456',
     ];
     // The recipe should apply cleanly.
     $this->applyRecipe($dir, options: $options);
@@ -35,7 +35,7 @@ class ComponentValidationTest extends BrowserTestBase {
     $this->assertStringContainsString('This value should not be blank.', $this->applyRecipe($dir, 1)->getErrorOutput());
     // It should also fail if an invalid tag ID is provided.
     $options = [
-      '--input=drupal_cms_analytics.property_id=nonsense',
+      '--input=drupal_cms_google_analytics.property_id=nonsense',
     ];
     $this->assertStringContainsString('This value is not valid.', $this->applyRecipe($dir, 1, $options)->getErrorOutput());
 
diff --git a/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php b/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
index d3b251d929bfc98d9d83834983028c7eed5cce6f..a29988ba4f4aa6e22d377e83ab603826f5113733 100644
--- a/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
+++ b/recipes/drupal_cms_starter/tests/src/Functional/ComponentValidationTest.php
@@ -85,7 +85,7 @@ class ComponentValidationTest extends BrowserTestBase {
     // Test that all the optional recipes can be applied on top of this one.
     foreach ($optional_recipes as $name) {
       $this->applyRecipe(InstalledVersions::getInstallPath($name), [
-        '--input=drupal_cms_analytics.property_id=GTM-123456',
+        '--input=drupal_cms_google_analytics.property_id=GTM-123456',
       ]);
     }