diff --git a/page_manager.services.yml b/page_manager.services.yml
index eaa0791..359b18f 100644
--- a/page_manager.services.yml
+++ b/page_manager.services.yml
@@ -19,6 +19,11 @@ services:
     arguments: ['@entity.manager', '@cache_tags.invalidator']
     tags:
       - { name: 'event_subscriber' }
+  page_manager.variant_route_filter:
+    class: Drupal\page_manager\Routing\VariantRouteFilter
+    arguments: ['@entity.manager']
+    tags:
+      - { name: route_filter }
   page_manager.executable_factory:
     class: Drupal\page_manager\PageExecutableFactory
   page_manager.route_name_response_subscriber:
diff --git a/src/Entity/PageViewBuilder.php b/src/Entity/PageViewBuilder.php
index 2cda880..ec9a962 100644
--- a/src/Entity/PageViewBuilder.php
+++ b/src/Entity/PageViewBuilder.php
@@ -9,7 +9,12 @@
 
 use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a view builder for page entities.
@@ -17,12 +22,35 @@
 class PageViewBuilder extends EntityViewBuilder {
 
   /**
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, RouteMatchInterface $route_match) {
+    parent::__construct($entity_type, $entity_manager, $language_manager);
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager'),
+      $container->get('language_manager'),
+      $container->get('current_route_match')
+    );
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
     $build = [];
     /** @var \Drupal\page_manager\PageInterface $entity */
-    if ($display_variant = $entity->getExecutable()->selectDisplayVariant()) {
+    if ($display_variant = $entity->getVariant($this->routeMatch->getParameter('variant_id'))) {
       if ($display_variant instanceof RefinableCacheableDependencyInterface) {
         $display_variant->addCacheableDependency($entity);
       }
diff --git a/src/EventSubscriber/RouteNameResponseSubscriber.php b/src/EventSubscriber/RouteNameResponseSubscriber.php
index 49c6e73..300a0c7 100644
--- a/src/EventSubscriber/RouteNameResponseSubscriber.php
+++ b/src/EventSubscriber/RouteNameResponseSubscriber.php
@@ -45,7 +45,8 @@ public function onResponse(FilterResponseEvent $event) {
     $response = $event->getResponse();
     if ($response instanceof CacheableResponseInterface) {
       $cacheability_metadata = $response->getCacheableMetadata();
-      $cacheability_metadata->addCacheTags(['page_manager_route_name:' . $this->routeMatch->getRouteName()]);
+      $route_name = $this->routeMatch->getParameter('base_route_name') ?: $this->routeMatch->getRouteName();
+      $cacheability_metadata->addCacheTags(['page_manager_route_name:' . $route_name]);
     }
   }
 
diff --git a/src/PageExecutable.php b/src/PageExecutable.php
index 6f950df..0e9976b 100644
--- a/src/PageExecutable.php
+++ b/src/PageExecutable.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\Context\ContextInterface;
 use Drupal\Core\Display\ContextAwareVariantInterface;
+use Drupal\Core\Display\VariantInterface;
 use Drupal\page_manager\Event\PageManagerContextEvent;
 use Drupal\page_manager\Event\PageManagerEvents;
 
@@ -25,13 +26,6 @@ class PageExecutable implements PageExecutableInterface {
   protected $page;
 
   /**
-   * The selected display variant.
-   *
-   * @var \Drupal\Core\Display\VariantInterface|null
-   */
-  protected $selectedDisplayVariant;
-
-  /**
    * An array of collected contexts.
    *
    * @var \Drupal\Component\Plugin\Context\ContextInterface[]
@@ -58,24 +52,6 @@ public function getPage() {
   /**
    * {@inheritdoc}
    */
-  public function selectDisplayVariant() {
-    if (!$this->selectedDisplayVariant) {
-      foreach ($this->page->getVariants() as $display_variant) {
-        if ($display_variant instanceof ContextAwareVariantInterface) {
-          $display_variant->setContexts($this->getContexts());
-        }
-        if ($display_variant->access()) {
-          $this->selectedDisplayVariant = $display_variant;
-          break;
-        }
-      }
-    }
-    return $this->selectedDisplayVariant;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function getContexts() {
     if (!$this->contexts) {
       $this->eventDispatcher()->dispatch(PageManagerEvents::PAGE_CONTEXT, new PageManagerContextEvent($this));
diff --git a/src/PageExecutableInterface.php b/src/PageExecutableInterface.php
index aeff68e..e1bd96c 100644
--- a/src/PageExecutableInterface.php
+++ b/src/PageExecutableInterface.php
@@ -27,18 +27,6 @@
   public function getPage();
 
   /**
-   * Selects the display variant to use for the page entity.
-   *
-   * This loops through the available display variants and checks each for
-   * access, returning the first one that is accessible.
-   *
-   * @return \Drupal\Core\Display\VariantInterface|null
-   *   Either the first accessible display variant, or NULL if none are
-   *   accessible.
-   */
-  public function selectDisplayVariant();
-
-  /**
    * Gets the values for all defined contexts.
    *
    * @return \Drupal\Component\Plugin\Context\ContextInterface[]
diff --git a/src/Routing/PageManagerRoutes.php b/src/Routing/PageManagerRoutes.php
index faa319f..1d16f1d 100644
--- a/src/Routing/PageManagerRoutes.php
+++ b/src/Routing/PageManagerRoutes.php
@@ -81,23 +81,27 @@ protected function alterRoutes(RouteCollection $collection) {
         $path = $entity->getPath();
       }
 
-      // Construct and add a new route.
-      $route = new Route(
-        $path,
-        [
-          '_entity_view' => 'page_manager_page',
-          'page_manager_page' => $entity_id,
-          '_title' => $entity->label(),
-        ],
-        [
-          '_entity_access' => 'page_manager_page.view',
-        ],
-        [
-          'parameters' => $parameters,
-          '_admin_route' => $entity->usesAdminTheme(),
-        ]
-      );
-      $collection->add($route_name, $route);
+      foreach ($entity->getVariants() as $variant_id => $variant) {
+        // Construct and add a new route.
+        $route = new Route(
+          $path,
+          [
+            '_entity_view' => 'page_manager_page',
+            'page_manager_page' => $entity_id,
+            '_title' => $entity->label(),
+            'variant_id' => $variant_id,
+            'base_route_name' => $route_name,
+          ],
+          [
+            '_entity_access' => 'page_manager_page.view',
+          ],
+          [
+            'parameters' => $parameters,
+            '_admin_route' => $entity->usesAdminTheme(),
+          ]
+        );
+        $collection->add($route_name . '_' . $variant_id, $route);
+      }
     }
   }
 
diff --git a/src/Routing/VariantRouteFilter.php b/src/Routing/VariantRouteFilter.php
new file mode 100644
index 0000000..dd06a23
--- /dev/null
+++ b/src/Routing/VariantRouteFilter.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\page_manager\Routing\VariantRouteFilter.
+ */
+
+namespace Drupal\page_manager\Routing;
+
+use Drupal\Core\Display\ContextAwareVariantInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Routing\RouteFilterInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @todo.
+ */
+class VariantRouteFilter implements RouteFilterInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $pageStorage;
+
+  /**
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->pageStorage = $entity_manager->getStorage('page');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    $parameters = $route->getOption('parameters');
+    return !empty($parameters['page_manager_page']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(RouteCollection $collection, Request $request) {
+    $new_collection = NULL;
+    $page = NULL;
+    foreach ($collection as $name => $route) {
+      /** @var \Symfony\Component\Routing\Route $route */
+      $defaults = $route->getDefaults();
+      if (isset($defaults['page_manager_page']) && isset($defaults['variant_id'])) {
+        /** @var \Drupal\page_manager\PageInterface $page */
+        $page = $page ?: $this->pageStorage->load($defaults['page_manager_page']);
+        $contexts = $page->getExecutable()->getContexts();
+
+        $variant = $page->getVariant($defaults['variant_id']);
+        if ($variant instanceof ContextAwareVariantInterface) {
+          $variant->setContexts($contexts);
+        }
+
+        if ($variant->access()) {
+          $new_collection = new RouteCollection();
+          $new_collection->add($name, $route);
+          break;
+        }
+        else {
+          $collection->remove($name);
+        }
+      }
+    }
+    return $new_collection ?: $collection;
+  }
+
+}
diff --git a/tests/src/Unit/PageExecutableTest.php b/tests/src/Unit/PageExecutableTest.php
index e8c967e..09061ad 100644
--- a/tests/src/Unit/PageExecutableTest.php
+++ b/tests/src/Unit/PageExecutableTest.php
@@ -57,24 +57,6 @@ public function testGetPage() {
   }
 
   /**
-   * @covers ::selectDisplayVariant
-   */
-  public function testSelectDisplayVariant() {
-    $display_variant1 = $this->prophesize(VariantInterface::class);
-    $display_variant1->access()->willReturn(FALSE);
-
-    $display_variant2 = $this->prophesize(VariantInterface::class);
-    $display_variant2->access()->willReturn(TRUE);
-
-    $this->page->getVariants()->willReturn([
-      'variant1' => $display_variant1->reveal(),
-      'variant2' => $display_variant2->reveal(),
-    ]);
-
-    $this->assertSame($display_variant2->reveal(), $this->exectuable->selectDisplayVariant());
-  }
-
-  /**
    * @covers ::addContext
    */
   public function testAddContext() {
diff --git a/tests/src/Unit/PageManagerRoutesTest.php b/tests/src/Unit/PageManagerRoutesTest.php
index 2194e1d..528f4cb 100644
--- a/tests/src/Unit/PageManagerRoutesTest.php
+++ b/tests/src/Unit/PageManagerRoutesTest.php
@@ -115,6 +115,8 @@ public function testAlterRoutesWithStatus() {
       ->shouldBeCalled();
     $page1->isFallbackPage()
       ->willReturn(FALSE);
+    $page1->getVariants()
+      ->willReturn(['variant1' => 'variant1']);
     $page1->label()
       ->willReturn('Page label')
       ->shouldBeCalled();
@@ -128,6 +130,8 @@ public function testAlterRoutesWithStatus() {
     $page2->status()
       ->willReturn(FALSE)
       ->shouldBeCalled();
+    $page2->getVariants()
+      ->willReturn(['variant2' => 'variant2']);
     $page2->isFallbackPage()->willReturn(FALSE);
     $page2->getPath()->willReturn('/page2');
     $pages['page2'] = $page2->reveal();
@@ -142,11 +146,14 @@ public function testAlterRoutesWithStatus() {
 
     // Only the valid page should be in the collection.
     $this->assertSame(1, $collection->count());
-    $route = $collection->get('page_manager.page_view_page1');
+    $route_name = 'page_manager.page_view_page1';
+    $route = $collection->get($route_name . '_variant1');
     $expected_defaults = [
       '_entity_view' => 'page_manager_page',
       'page_manager_page' => 'page1',
       '_title' => 'Page label',
+      'variant_id' => 'variant1',
+      'base_route_name' => $route_name,
     ];
     $expected_requirements = [
       '_entity_access' => 'page_manager_page.view',
@@ -181,6 +188,8 @@ public function testAlterRoutesOverrideExisting($page_path, $existing_route_path
     $page->getPath()
       ->willReturn($page_path)
       ->shouldBeCalled();
+    $page->getVariants()
+      ->willReturn(['variant1' => 'variant1']);
     $page->isFallbackPage()->willReturn(FALSE);
     $page->label()->willReturn(NULL);
     $page->usesAdminTheme()->willReturn(FALSE);
@@ -198,13 +207,15 @@ public function testAlterRoutesOverrideExisting($page_path, $existing_route_path
 
     // The normal route name is not used, the existing route name is instead.
     $this->assertSame(1, $collection->count());
-    $this->assertNull($collection->get('page_manager.page_view_page1'));
+    $this->assertNull($collection->get('page_manager.page_view_page1_variant1'));
 
-    $route = $collection->get($route_name);
+    $route = $collection->get($route_name . '_variant1');
     $expected_defaults = [
       '_entity_view' => 'page_manager_page',
       'page_manager_page' => 'page1',
       '_title' => NULL,
+      'variant_id' => 'variant1',
+      'base_route_name' => $route_name,
     ];
     $expected_requirements = [
       '_entity_access' => 'page_manager_page.view',
diff --git a/tests/src/Unit/RouteNameResponseSubscriberTest.php b/tests/src/Unit/RouteNameResponseSubscriberTest.php
index 6fafff9..ac2c78a 100644
--- a/tests/src/Unit/RouteNameResponseSubscriberTest.php
+++ b/tests/src/Unit/RouteNameResponseSubscriberTest.php
@@ -32,6 +32,7 @@ public function testOnResponseCacheable() {
 
     $route_name = 'the_route_name';
     $master_route_match = $this->prophesize(RouteMatchInterface::class);
+    $master_route_match->getParameter('base_route_name')->willReturn(NULL);
     $master_route_match->getRouteName()->willReturn($route_name);
     $current_route_match = $this->prophesize(StackedRouteMatchInterface::class);
     $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal());
@@ -51,6 +52,7 @@ public function testOnResponseUncacheable() {
     $event = $this->buildEvent($response);
 
     $master_route_match = $this->prophesize(RouteMatchInterface::class);
+    $master_route_match->getParameter()->shouldNotBeCalled();
     $master_route_match->getRouteName()->shouldNotBeCalled();
     $current_route_match = $this->prophesize(StackedRouteMatchInterface::class);
     $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal());
@@ -60,6 +62,27 @@ public function testOnResponseUncacheable() {
   }
 
   /**
+   * @covers ::onResponse
+   */
+  public function testOnResponseCacheableWithBaseRouteName() {
+    $response = new CacheableResponse('');
+    $event = $this->buildEvent($response);
+
+    $route_name = 'the_route_name';
+    $master_route_match = $this->prophesize(RouteMatchInterface::class);
+    $master_route_match->getParameter('base_route_name')->willReturn($route_name);
+    $master_route_match->getRouteName()->shouldNotBeCalled();
+    $current_route_match = $this->prophesize(StackedRouteMatchInterface::class);
+    $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal());
+
+    $subscriber = new RouteNameResponseSubscriber($current_route_match->reveal());
+    $subscriber->onResponse($event);
+
+    $expected = ["page_manager_route_name:$route_name"];
+    $this->assertSame($expected, $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
    * Builds an event to wrap a response.
    *
    * @param \Symfony\Component\HttpFoundation\Response $response
