diff --git a/core/modules/block/block.admin.inc b/core/modules/block/block.admin.inc
index f94ce25..0b0edd4 100644
--- a/core/modules/block/block.admin.inc
+++ b/core/modules/block/block.admin.inc
@@ -8,16 +8,6 @@
 use Drupal\block\Plugin\Core\Entity\Block;
 
 /**
- * Page callback: Attaches CSS for the block region demo.
- *
- * @see block_menu()
- */
-function block_admin_demo($theme = NULL) {
-  drupal_add_css(drupal_get_path('module', 'block') . '/block.admin.css');
-  return '';
-}
-
-/**
  * Page callback: Shows the block administration page.
  *
  * @param string $theme
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index d94ec3b..dc0ce35 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -170,19 +170,16 @@ function block_menu() {
         'access arguments' => array($key),
         'file' => 'block.admin.inc',
       );
-      $items['admin/structure/block/demo/' . $key] = array(
-        'title' => check_plain($theme->info['name']),
-        'page callback' => 'block_admin_demo',
-        'page arguments' => array($key),
-        'type' => MENU_CALLBACK,
-        'access callback' => '_block_themes_access',
-        'access arguments' => array($key),
-        'theme callback' => '_block_custom_theme',
-        'theme arguments' => array($key),
-        'file' => 'block.admin.inc',
-      );
     }
   }
+  if (isset($theme)) {
+    $items['admin/structure/block/demo/%'] = array(
+      'route_name' => 'block_admin_demo',
+      'type' => MENU_CALLBACK,
+      'theme callback' => '_block_custom_theme',
+      'theme arguments' => array(4),
+    );
+  }
   return $items;
 }
 
@@ -191,7 +188,6 @@ function block_menu() {
  *
  * Path:
  * - admin/structure/block/list/% (for each theme)
- * - admin/structure/block/demo/% (for each theme)
  *
  * @param $theme
  *   Either the name of a theme or a full theme object.
@@ -234,9 +230,8 @@ function block_page_build(&$page) {
 
   // Fetch a list of regions for the current theme.
   $all_regions = system_region_list($theme);
-
   $item = menu_get_item();
-  if ($item['path'] != 'admin/structure/block/demo/' . $theme) {
+  if ($item['path'] != 'admin/structure/block/demo/%' || !isset($item['map'][4]) || $item['map'][4] != $theme) {
     // Load all region content assigned via blocks.
     foreach (array_keys($all_regions) as $region) {
       // Assign blocks to region.
@@ -256,26 +251,23 @@ function block_page_build(&$page) {
   }
   else {
     // Append region description if we are rendering the regions demo page.
-    $item = menu_get_item();
-    if ($item['path'] == 'admin/structure/block/demo/' . $theme) {
-      $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
-      foreach ($visible_regions as $region) {
-        $description = '<div class="block-region">' . $all_regions[$region] . '</div>';
-        $page[$region]['block_description'] = array(
-          '#markup' => $description,
-          '#weight' => 15,
-        );
-      }
-      $page['page_top']['backlink'] = array(
-        '#type' => 'link',
-        '#title' => t('Exit block region demonstration'),
-        '#href' => 'admin/structure/block' . (config('system.theme')->get('default') == $theme ? '' : '/list/' . $theme),
-        // Add the "overlay-restore" class to indicate this link should restore
-        // the context in which the region demonstration page was opened.
-        '#options' => array('attributes' => array('class' => array('block-demo-backlink', 'overlay-restore'))),
-        '#weight' => -10,
+    $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
+    foreach ($visible_regions as $region) {
+      $description = '<div class="block-region">' . $all_regions[$region] . '</div>';
+      $page[$region]['block_description'] = array(
+        '#markup' => $description,
+        '#weight' => 15,
       );
     }
+    $page['page_top']['backlink'] = array(
+      '#type' => 'link',
+      '#title' => t('Exit block region demonstration'),
+      '#href' => 'admin/structure/block' . (config('system.theme')->get('default') == $theme ? '' : '/list/' . $theme),
+      // Add the "overlay-restore" class to indicate this link should restore
+      // the context in which the region demonstration page was opened.
+      '#options' => array('attributes' => array('class' => array('block-demo-backlink', 'overlay-restore'))),
+      '#weight' => -10,
+    );
   }
 }
 
diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml
index af247b0..810a96d 100644
--- a/core/modules/block/block.routing.yml
+++ b/core/modules/block/block.routing.yml
@@ -4,3 +4,10 @@ block_admin_block_delete:
     _form: '\Drupal\block\Form\AdminBlockDeleteForm'
   requirements:
     _permission: 'administer blocks'
+
+block_admin_demo:
+  pattern: '/admin/structure/block/demo/{theme}'
+  defaults:
+    _content: '\Drupal\block\Controller\AdminController::demo'
+  requirements:
+    _access_block_theme: 'TRUE'
diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml
index 745bf66..92906a4 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -9,3 +9,7 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [block]
+  access_check.block.demo:
+    class: Drupal\block\Access\DemoThemesAccessCheck
+    tags:
+      - { name: access_check }
diff --git a/core/modules/block/lib/Drupal/block/Access/DemoThemesAccessCheck.php b/core/modules/block/lib/Drupal/block/Access/DemoThemesAccessCheck.php
new file mode 100644
index 0000000..6f58c01
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Access/DemoThemesAccessCheck.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Access\DemoThemesAccessCheck.
+ */
+
+namespace Drupal\block\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Access check for block theme demo routes.
+ */
+class DemoThemesAccessCheck implements AccessCheckInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_access_block_theme', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    return user_access('administer blocks') && drupal_theme_access($request->attributes->get('theme'));
+  }
+}
diff --git a/core/modules/block/lib/Drupal/block/Controller/AdminController.php b/core/modules/block/lib/Drupal/block/Controller/AdminController.php
new file mode 100644
index 0000000..d964fd9
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Controller/AdminController.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\block\Controller\AdminController.
+ */
+
+namespace Drupal\block\Controller;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Controller routines for admin block routes.
+ */
+class AdminController {
+
+  /**
+   * Returns a block theme demo page.
+   *
+   * @return string
+   *   A HTML-formatted string with the block theme demo page content.
+   *
+   */
+  public function demo(Request $request) {
+    $themes = list_themes();
+    $theme = $themes[$request->attributes->get('theme')];
+
+    // @todo Remove use of drupal_set_title() when
+    //   http://drupal.org/node/1871596 is in.
+    drupal_set_title(t($theme->info['name']), PASS_THROUGH);
+    drupal_add_css(drupal_get_path('module', 'block') . '/block.admin.css');
+    return new Response();
+  }
+
+}
