 core/authorize.php                                 |   3 +-
 core/core.services.yml                             |  58 +++--
 core/includes/batch.inc                            |   3 +-
 core/includes/common.inc                           | 141 -----------
 core/includes/errors.inc                           |   3 +-
 core/includes/install.core.inc                     |   3 +-
 core/includes/menu.inc                             |  10 +-
 core/includes/theme.inc                            |  91 +++----
 core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php |  93 -------
 .../Core/Block/MainContentBlockPluginInterface.php |  25 ++
 core/lib/Drupal/Core/ContentNegotiation.php        |   1 +
 core/lib/Drupal/Core/Controller/AjaxController.php | 104 ++++----
 .../Drupal/Core/Controller/DialogController.php    | 142 +++--------
 core/lib/Drupal/Core/Controller/HtmlController.php | 277 +++++++++++++++++++++
 .../Drupal/Core/Controller/HtmlControllerBase.php  |  79 ------
 .../Drupal/Core/Controller/HtmlPageController.php  |  81 ------
 .../Core/Controller/MainContentControllerBase.php  |  87 +++++++
 .../Controller/MainContentControllerInterface.php  |  92 +++++++
 .../lib/Drupal/Core/Controller/ModalController.php |  30 +++
 core/lib/Drupal/Core/CoreServiceProvider.php       |   3 +
 .../Core/Display/Annotation/DisplayVariant.php     |  11 +-
 .../Core/Display/Annotation/PageDisplayVariant.php |  26 ++
 .../Drupal/Core/Display/PageVariantInterface.php   |  30 +++
 .../ContentControllerSubscriber.php                |  62 +++--
 .../CustomPageExceptionHtmlSubscriber.php          |  74 ++----
 .../DefaultExceptionHtmlSubscriber.php             |  90 +++----
 .../EventSubscriber/DefaultExceptionSubscriber.php |  59 +----
 .../Core/EventSubscriber/HtmlViewSubscriber.php    | 110 --------
 .../EventSubscriber/MaintenanceModeSubscriber.php  |  21 +-
 .../Drupal/Core/EventSubscriber/ViewSubscriber.php |  40 +--
 .../Core/Page/DefaultHtmlFragmentRenderer.php      | 155 ------------
 .../Drupal/Core/Page/DefaultHtmlPageRenderer.php   | 125 ----------
 core/lib/Drupal/Core/Page/FeedLinkElement.php      |  32 ---
 core/lib/Drupal/Core/Page/HeadElement.php          | 100 --------
 core/lib/Drupal/Core/Page/HtmlFragment.php         | 229 -----------------
 .../lib/Drupal/Core/Page/HtmlFragmentInterface.php |  66 -----
 .../Core/Page/HtmlFragmentRendererInterface.php    |  38 ---
 core/lib/Drupal/Core/Page/HtmlPage.php             | 234 -----------------
 .../Drupal/Core/Page/HtmlPageRendererInterface.php |  29 ---
 core/lib/Drupal/Core/Page/LinkElement.php          |  40 ---
 core/lib/Drupal/Core/Page/MetaElement.php          |  81 ------
 core/lib/Drupal/Core/Page/RenderHtmlRenderer.php   |  88 -------
 .../Core/Page/RenderHtmlRendererInterface.php      |  31 ---
 .../Drupal/Core/Render/BareHtmlPageRenderer.php    |  78 ++++++
 .../Core/Render/BareHtmlPageRendererInterface.php  |  72 ++++++
 core/lib/Drupal/Core/Render/Element/Html.php       | 107 +++++++-
 core/lib/Drupal/Core/Render/Element/Page.php       |   6 +-
 .../Core/Render/MainContentControllerPass.php      |  33 +++
 .../src/Authentication/Provider/BasicAuth.php      |   2 +-
 core/modules/block/block.module                    |  35 +--
 core/modules/block/block.services.yml              |   5 +
 .../BlockPageDisplayVariantSubscriber.php          |  64 +++++
 .../{FullPageVariant.php => BlockPageVariant.php}  |  45 +++-
 .../Plugin/DisplayVariant/DemoBlockPageVariant.php | 134 ++++++++++
 core/modules/block/src/Tests/BlockTest.php         |   5 +-
 ...ageVariantTest.php => BlockPageVariantTest.php} | 115 ++++++---
 .../modules/node/src/Tests/Views/FrontPageTest.php |   2 +-
 core/modules/rest/src/Tests/DeleteTest.php         |   2 +-
 core/modules/simpletest/simpletest.module          |   1 -
 .../system/src/Controller/BatchController.php      |  92 +------
 .../system/src/Controller/DbUpdateController.php   |  19 +-
 .../system/src/Controller/Http4xxController.php    |  41 +++
 .../src/Event/PageDisplayVariantSelectionEvent.php |  55 ++++
 core/modules/system/src/Event/SystemEvents.php     |  23 ++
 .../system/src/Plugin/Block/SystemMainBlock.php    |  21 +-
 .../Plugin/DisplayVariant/SimplePageVariant.php    |  47 ++++
 .../system/src/Tests/Common/AddFeedTest.php        |  18 +-
 .../system/src/Tests/Common/PageRenderTest.php     |   7 +-
 .../src/Tests/Routing/ExceptionHandlingTest.php    |  27 +-
 .../src/Tests/System/MainContentFallbackTest.php   |  85 -------
 core/modules/system/system.routing.yml             |  20 +-
 core/modules/system/templates/html.html.twig       |  14 +-
 .../modules/ajax_forms_test/ajax_forms_test.module |   4 +-
 .../src/Controller/EntityTestController.php        |   2 +-
 .../src/Controller/ErrorTestController.php         |   2 +-
 .../tests/modules/menu_test/menu_test.module       |   4 +-
 .../modules/menu_test/src/TestControllers.php      |  12 +-
 .../src/Controller/ModuleTestController.php        |   2 +-
 .../paramconverter_test/src/TestControllers.php    |   6 +-
 .../router_test_directory/src/TestContent.php      |   6 +-
 .../router_test_directory/src/TestControllers.php  |   8 +-
 .../src/Controller/SessionTestController.php       |  16 +-
 .../src/EventSubscriber/HtmlPageSubscriber.php     |  45 ----
 .../system_module_test/system_module_test.module   |  20 ++
 .../system_module_test.services.yml                |   5 -
 .../src/Controller/SystemTestController.php        |   8 +-
 .../tests/modules/system_test/system_test.module   |  19 +-
 .../modules/theme_test/src/ThemeTestController.php |   6 +-
 .../src/TwigThemeTestController.php                |   2 +-
 core/modules/system/theme.api.php                  |  61 +++++
 core/modules/views/views.module                    |  24 +-
 .../Tests/Core/Ajax/AjaxResponseRendererTest.php   | 132 ----------
 .../Tests/Core/Controller/AjaxControllerTest.php   | 118 +++++++++
 core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php |  62 -----
 core/themes/bartik/bartik.theme                    |  56 ++---
 core/themes/seven/css/theme/install-page.css       |   2 +-
 core/themes/seven/css/theme/maintenance-page.css   |   2 +-
 core/themes/seven/seven.theme                      |  55 ++--
 98 files changed, 2029 insertions(+), 2922 deletions(-)

diff --git a/core/authorize.php b/core/authorize.php
index 4b0f249..22bede6 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -25,7 +25,6 @@
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Drupal\Core\Site\Settings;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
 
 // Change the directory to the Drupal root.
 chdir('..');
@@ -150,7 +149,7 @@ function authorize_access_allowed() {
 
 if (!empty($output)) {
   $response->headers->set('Content-Type', 'text/html; charset=utf-8');
-  $response->setContent(DefaultHtmlPageRenderer::renderPage($output, $page_title, 'maintenance', array(
+  $response->setContent(\Drupal::service('bare_html_page_renderer')->renderMaintenancePage($output, $page_title, array(
     '#show_messages' => $show_messages,
   )));
   $response->send();
diff --git a/core/core.services.yml b/core/core.services.yml
index 7f9ed9b..298dedd 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -633,7 +633,7 @@ services:
       - { name: route_enhancer, priority: 20 }
   route_content_controller_subscriber:
     class: Drupal\Core\EventSubscriber\ContentControllerSubscriber
-    arguments: ['@content_negotiation']
+    arguments: ['@content_negotiation', '%main_content_controllers%']
     tags:
       - { name: event_subscriber }
   route_content_form_controller_subscriber:
@@ -650,17 +650,32 @@ services:
     class: Drupal\Core\EventSubscriber\RouteMethodSubscriber
     tags:
       - { name: event_subscriber }
-  controller.page:
-    class: Drupal\Core\Controller\HtmlPageController
-    arguments: ['@controller_resolver', '@title_resolver', '@render_html_renderer']
-  controller.ajax:
+  main_content_controller.base:
+    abstract: true
+    arguments: ['@controller_resolver']
+  main_content_controller.html:
+    class: Drupal\Core\Controller\HtmlController
+    parent: main_content_controller.base
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher']
+    tags:
+      - { name: main_content_controller, format: html }
+  main_content_controller.ajax:
     class: Drupal\Core\Controller\AjaxController
-    arguments: ['@controller_resolver', '@ajax_response_renderer']
-  controller.dialog:
+    parent: main_content_controller.base
+    arguments: ['@element_info']
+    tags:
+      - { name: main_content_controller, format: drupal_ajax }
+  main_content_controller.dialog:
     class: Drupal\Core\Controller\DialogController
-    arguments: ['@controller_resolver', '@title_resolver']
-  ajax_response_renderer:
-    class: Drupal\Core\Ajax\AjaxResponseRenderer
+    parent: main_content_controller.base
+    arguments: ['@title_resolver']
+    tags:
+      - { name: main_content_controller, format: drupal_dialog }
+  main_content_controller.modal:
+    class: Drupal\Core\Controller\ModalController
+    parent: main_content_controller.dialog
+    tags:
+      - { name: main_content_controller, format: drupal_modal }
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
@@ -672,20 +687,9 @@ services:
     class: Drupal\Core\EventSubscriber\ViewSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@content_negotiation', '@title_resolver', '@ajax_response_renderer']
-  html_view_subscriber:
-    class: Drupal\Core\EventSubscriber\HtmlViewSubscriber
-    tags:
-      - { name: event_subscriber }
-    arguments: ['@html_fragment_renderer', '@html_page_renderer']
-  render_html_renderer:
-    class: Drupal\Core\Page\RenderHtmlRenderer
-    arguments: ['@url_generator']
-  html_fragment_renderer:
-    class: Drupal\Core\Page\DefaultHtmlFragmentRenderer
-    arguments: ['@language_manager']
-  html_page_renderer:
-    class: Drupal\Core\Page\DefaultHtmlPageRenderer
+    arguments: ['@content_negotiation', '@title_resolver']
+  bare_html_page_renderer:
+    class: Drupal\Core\Render\BareHtmlPageRenderer
   private_key:
     class: Drupal\Core\PrivateKey
     arguments: ['@state']
@@ -734,7 +738,7 @@ services:
     arguments: ['@state', '@current_user']
   maintenance_mode_subscriber:
     class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber
-    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user']
+    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer']
     tags:
       - { name: event_subscriber }
   path_subscriber:
@@ -774,12 +778,12 @@ services:
     class: Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@html_fragment_renderer', '@html_page_renderer']
+    arguments: ['@http_kernel']
   exception.default:
     class: Drupal\Core\EventSubscriber\DefaultExceptionSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@html_fragment_renderer', '@html_page_renderer', '@config.factory']
+    arguments: ['@config.factory', '@bare_html_page_renderer']
   exception.logger:
     class: Drupal\Core\EventSubscriber\ExceptionLoggingSubscriber
     tags:
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 68fc14b..1ba845f 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -19,7 +19,6 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Batch\Percentage;
 use Drupal\Core\Form\FormState;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -136,7 +135,7 @@ function _batch_progress_page() {
     // additional HTML output by PHP shows up inside the page rather than below
     // it. While this causes invalid HTML, the same would be true if we didn't,
     // as content is not allowed to appear after </html> anyway.
-    $fallback = DefaultHtmlPageRenderer::renderPage($fallback, $current_set['title'], 'maintenance', array(
+    $fallback = \Drupal::service('bare_html_page_renderer')->renderMaintenancePage($fallback, $current_set['title'], array(
       '#show_messages' => FALSE,
     ));
     list($fallback) = explode('<!--partial-->', $fallback);
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 020d84d..2fc5221 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1117,9 +1117,6 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE, $theme_add_css = TRUE)
     '#type' => 'styles',
     '#items' => $css,
   );
-  if (!empty($setting)) {
-    $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
-  }
 
   return drupal_render($styles);
 }
@@ -2262,39 +2259,6 @@ function drupal_page_set_cache(Response $response, Request $request) {
 }
 
 /**
- * Sets the main page content value for later use.
- *
- * Given the nature of the Drupal page handling, this will be called once with
- * a string or array. We store that and return it later as the block is being
- * displayed.
- *
- * @param $content
- *   A string or renderable array representing the body of the page.
- *
- * @return
- *   If called without $content, a renderable array representing the body of
- *   the page.
- */
-function drupal_set_page_content($content = NULL) {
-  $content_block = &drupal_static(__FUNCTION__, NULL);
-  $main_content_display = &drupal_static('system_main_content_added', FALSE);
-
-  // Filter out each empty value, though allow '0' and 0, which would be
-  // filtered out by empty().
-  if ($content !== NULL && $content !== '') {
-    $content_block = (is_array($content) ? $content : array('main' => array('#markup' => $content)));
-  }
-  else {
-    // Indicate that the main content has been requested. We assume that
-    // the module requesting the content will be adding it to the page.
-    // A module can indicate that it does not handle the content by setting
-    // the static variable back to FALSE after calling this function.
-    $main_content_display = TRUE;
-    return $content_block;
-  }
-}
-
-/**
  * Pre-render callback: Renders a link into #markup.
  *
  * @deprecated Use \Drupal\Core\Render\Element\Link::preRenderLink().
@@ -2399,111 +2363,6 @@ function drupal_pre_render_links($element) {
 }
 
 /**
- * Processes the page render array, enhancing it as necessary.
- *
- * @param $page
- *   A string or array representing the content of a page. The array consists of
- *   the following keys:
- *   - #type: Value is always 'page'. This pushes the theming through
- *     the page template (required).
- *   - #show_messages: Suppress drupal_get_message() items. Used by Batch
- *     API (optional).
- *
- * @return array
- *   The processed render array for the page.
- *
- * @see hook_page_attachments()
- * @see hook_page_attachments_alter()
- * @see hook_page_top()
- * @see hook_page_bottom()
- * @see element_info()
- */
-function drupal_prepare_page($page) {
-  $main_content_display = &drupal_static('system_main_content_added', FALSE);
-
-  // Pull out the page title to set it back later.
-  if (is_array($page) && isset($page['#title'])) {
-    $title = $page['#title'];
-  }
-
-  // Allow menu callbacks to return strings or arbitrary arrays to render.
-  // If the array returned is not of #type page directly, we need to fill
-  // in the page with defaults.
-  if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) {
-    drupal_set_page_content($page);
-    $page = element_info('page');
-  }
-
-  // Modules can add attachments.
-  $attachments = [];
-  foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) {
-    $function = $module . '_page_attachments';
-    $function($attachments);
-  }
-  if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) {
-    throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments().');
-  }
-  // Modules and themes can alter page attachments.
-  \Drupal::moduleHandler()->alter('page_attachments', $attachments);
-  \Drupal::theme()->alter('page_attachments', $attachments);
-  if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) {
-    throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments_alter().');
-  }
-  if (isset($attachments['#attached'])) {
-    $page['#attached'] = $attachments['#attached'];
-  }
-  if (isset($attachments['#post_render_cache'])) {
-    $page['#post_render_cache'] = $attachments['#post_render_cache'];
-  }
-
-  // Modules can add renderable arrays to the top and bottom of the page.
-  $pseudo_page_top = [];
-  $pseudo_page_bottom = [];
-  foreach (\Drupal::moduleHandler()->getImplementations('page_top') as $module) {
-    $function = $module . '_page_top';
-    $function($pseudo_page_top);
-  }
-  foreach (\Drupal::moduleHandler()->getImplementations('page_bottom') as $module) {
-    $function = $module . '_page_bottom';
-    $function($pseudo_page_bottom);
-  }
-  if (!empty($pseudo_page_top)) {
-    $page['page_top'] = $pseudo_page_top;
-  }
-  if (!empty($pseudo_page_bottom)) {
-    $page['page_bottom'] = $pseudo_page_bottom;
-  }
-
-  // @todo Clean this up as part of https://www.drupal.org/node/2352155.
-  if (\Drupal::moduleHandler()->moduleExists('block')) {
-    _block_page_build($page);
-    // Find all non-empty page regions, and add a theme wrapper function that
-    // allows them to be consistently themed.
-    $regions = system_region_list(\Drupal::theme()->getActiveTheme()->getName());
-    foreach (array_keys($regions) as $region) {
-      if (!empty($page[$region])) {
-        $page[$region]['#theme_wrappers'][] = 'region';
-        $page[$region]['#region'] = $region;
-      }
-    }
-  }
-
-  // If no module has taken care of the main content, add it to the page now.
-  // This allows the site to still be usable even if no modules that
-  // control page regions (for example, the Block module) are enabled.
-  if (!$main_content_display) {
-    $page['content']['system_main'] = drupal_set_page_content();
-  }
-
-  // Set back the previously stored title.
-  if (isset($title)) {
-    $page['#title'] = $title;
-  }
-
-  return $page;
-}
-
-/**
  * Renders final HTML given a structured array tree.
  *
  * Calls drupal_render() in such a way that #post_render_cache callbacks are
diff --git a/core/includes/errors.inc b/core/includes/errors.inc
index 4065408..be30937 100644
--- a/core/includes/errors.inc
+++ b/core/includes/errors.inc
@@ -8,7 +8,6 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Logger\RfcLogLevel;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
 use Drupal\Core\Utility\Error;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -236,7 +235,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
         install_display_output($output, $GLOBALS['install_state']);
       }
       else {
-        $output = DefaultHtmlPageRenderer::renderPage($message, 'Error');
+        $output = \Drupal::service('bare_html_page_renderer')->renderMaintenancePage($message, 'Error');
       }
 
       $response = new Response($output, 500);
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index e494862..7bdf888 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -14,7 +14,6 @@
 use Drupal\Core\Installer\InstallerKernel;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\StringTranslation\Translator\FileTranslation;
 use Drupal\Core\Extension\ExtensionDiscovery;
@@ -934,7 +933,7 @@ function install_display_output($output, $install_state) {
     'ETag' => '"' . REQUEST_TIME . '"',
   );
   $response->headers->add($default_headers);
-  $response->setContent(DefaultHtmlPageRenderer::renderPage($output, $output['#title'], 'install', $regions));
+  $response->setContent(\Drupal::service('bare_html_page_renderer')->renderInstallPage($output, $output['#title'], $regions));
   $response->send();
   exit;
 }
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 76aafd0..b42ff1c 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -110,13 +110,15 @@
  * class and method. Page controller classes do not necessarily need to
  * implement any particular interface or extend any particular base class. The
  * only requirement is that the method specified in your *.routing.yml file
- * return one of the following, depending on whether you specified _content or
- * _controller in the routing file defaults section:
+ * returns:
  * - A render array (see the
  *   @link theme_render Theme and render topic @endlink for more information),
  *   if _content is used in the routing file.
- * - A \Drupal\Core\Page\HtmlFragmentInterface object (fragment or page), if
- *   _content is used in the routing file.
+ *   This render array is then rendered in the requested format (HTML, dialog,
+ *   modal, AJAX are supported by default). In the case of HTML, it will be
+ *   surrounded by blocks by default: the Block module is enabled by default,
+ *   and hence its Page Display Variant that surrounds the main content with
+ *   blocks is also used by default.
  * - A \Symfony\Component\HttpFoundation\Response object, if _controller is
  *   used in the routing file.
  * As a note, if your module registers multiple simple routes, it is usual
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 15d62a8..944dc9e 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -17,8 +17,6 @@
 use Drupal\Core\Config\StorageException;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionNameLengthException;
-use Drupal\Core\Page\LinkElement;
-use Drupal\Core\Page\MetaElement;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Theme\ThemeSettings;
 use Drupal\Component\Utility\NestedArray;
@@ -1645,22 +1643,24 @@ function drupal_pre_render_html(array $element) {
  *   - page: A render element representing the page.
  */
 function template_preprocess_html(&$variables) {
-  /** @var $page \Drupal\Core\Page\HtmlPage */
-  $page = $variables['page_object'];
+  $variables['html_attributes'] = new Attribute();
 
-  $variables['html_attributes'] = $page->getHtmlAttributes();
-  $variables['attributes'] = $page->getBodyAttributes();
-  $variables['page'] = $page;
+  // HTML element attributes.
+  $language_interface = \Drupal::languageManager()->getCurrentLanguage();
+  $variables['html_attributes']['lang'] = $language_interface->getId();
+  $variables['html_attributes']['dir'] = $language_interface->getDirection();
 
   // Compile a list of classes that are going to be applied to the body element.
   // This allows advanced theming based on context (home page, node of certain
   // type, etc.).
-  $body_classes = $variables['attributes']['class'];
+  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
+    $variables['attributes']['class'][] = 'db-offline';
+  }
 
   // Add a class that tells us whether the page is viewed by an authenticated
   // user.
   if ($variables['logged_in']) {
-    $body_classes[] = 'user-logged-in';
+    $variables['attributes']['class'][] = 'user-logged-in';
   }
   // Add a class that tells us what path the page is located make it possible
   // to theme the page depending on the current path (e.g. node, admin, user,
@@ -1668,20 +1668,18 @@ function template_preprocess_html(&$variables) {
   $path = \Drupal::request()->getPathInfo();
 
   if (drupal_is_front_page()) {
-    $body_classes[] = 'path-frontpage';
+    $variables['attributes']['class'][] = 'path-frontpage';
   }
   else {
     $segment = explode('/', $path);
-    $body_classes[] = 'path-' . drupal_html_class($segment[1]);
+    $variables['attributes']['class'][] = 'path-' . drupal_html_class($segment[1]);
   }
 
-  $variables['attributes']['class'] = $body_classes;
-
   $site_config = \Drupal::config('system.site');
   // Construct page title.
-  if ($page->hasTitle()) {
+  if (!empty($variables['html']['page']['#title'])) {
     $head_title = array(
-      'title' => SafeMarkup::set(trim(strip_tags($page->getTitle()))),
+      'title' => SafeMarkup::set(trim(strip_tags($variables['html']['page']['#title']))),
       'name' => String::checkPlain($site_config->get('name')),
     );
   }
@@ -1709,35 +1707,27 @@ function template_preprocess_html(&$variables) {
   }
   $variables['head_title'] = SafeMarkup::set($output);
 
-  // @todo Remove drupal_*_html_head() and refactor accordingly.
-  $html_heads = drupal_get_html_head(FALSE);
-  uasort($html_heads, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
-  foreach ($html_heads as $name => $tag) {
-    if ($tag['#tag'] == 'link') {
-      $link = new LinkElement($name, isset($tag['#attributes']['content']) ? $tag['#attributes']['content'] : NULL, $tag['#attributes']);
-      if (!empty($tag['#noscript'])) {
-        $link->setNoScript();
-      }
-      $page->addLinkElement($link);
-    }
-    elseif ($tag['#tag'] == 'meta') {
-      $metatag = new MetaElement(NULL, $tag['#attributes']);
-      if (!empty($tag['#noscript'])) {
-        $metatag->setNoScript();
-      }
-      $page->addMetaElement($metatag);
-    }
+  // Collect all attachments. This must happen in the preprocess function for
+  // #type => html, to ensure that attachments added in #pre_render callbacks
+  // for #type => html are included.
+  $attached = $variables['html']['#attached'];
+  $attached = drupal_merge_attached($attached, $variables['html']['page']['#attached']);
+  if (isset($variables['html']['page_top'])) {
+    $attached = drupal_merge_attached($attached, $variables['html']['page_top']['#attached']);
   }
-
-  // Add favicon.
-  if (theme_get_setting('features.favicon')) {
-    $url = UrlHelper::stripDangerousProtocols(theme_get_setting('favicon.url'));
-    $link = new LinkElement($url, 'shortcut icon', ['type' => theme_get_setting('favicon.mimetype')]);
-    $page->addLinkElement($link);
+  if (isset($variables['html']['page_bottom'])) {
+    $attached = drupal_merge_attached($attached, $variables['html']['page_bottom']['#attached']);
   }
 
-  $variables['page_top'][] = array('#markup' => $page->getBodyTop());
-  $variables['page_bottom'][] = array('#markup' => $page->getBodyBottom());
+  // Render the attachments into HTML markup to be used directly in the template
+  // for #type => html: html.html.twig.
+  $all_attached = ['#attached' => $attached];
+  drupal_process_attached($all_attached);
+
+  $variables['html']['styles'] = drupal_get_css();
+  $variables['html']['scripts'] = drupal_get_js();
+  $variables['html']['scripts_bottom'] = drupal_get_js('footer');
+  $variables['html']['head'] = drupal_get_html_head(FALSE);
 }
 
 /**
@@ -1748,8 +1738,6 @@ function template_preprocess_html(&$variables) {
  * Most themes use their own copy of page.html.twig. The default is located
  * inside "modules/system/page.html.twig". Look in there for the full list of
  * variables.
- *
- * @see DefaultHtmlFragmentRenderer::render()
  */
 function template_preprocess_page(&$variables) {
   $language_interface = \Drupal::languageManager()->getCurrentLanguage();
@@ -1889,15 +1877,6 @@ function template_preprocess_maintenance_page(&$variables) {
   // @todo Rename the templates to page--maintenance + page--install.
   template_preprocess_page($variables);
 
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getBodyAttributes();
-  $classes = $attributes['class'];
-  $classes[] = 'maintenance-page';
-  if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
-    $classes[] = 'db-offline';
-  }
-  $attributes['class'] = $classes;
-
   // @see system_page_attachments()
   $variables['#attached']['library'][] = 'core/normalize';
   $variables['#attached']['library'][] = 'system/maintenance';
@@ -1917,12 +1896,6 @@ function template_preprocess_maintenance_page(&$variables) {
 function template_preprocess_install_page(&$variables) {
   template_preprocess_maintenance_page($variables);
 
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getBodyAttributes();
-  $classes = $attributes['class'];
-  $classes[] = 'install-page';
-  $attributes['class'] = $classes;
-
   // Override the site name that is displayed on the page, since Drupal is
   // still in the process of being installed.
   $distribution_name = String::checkPlain(drupal_install_profile_distribution_name());
@@ -2160,7 +2133,7 @@ function drupal_common_theme() {
   return array(
     // From theme.inc.
     'html' => array(
-      'variables' => array('page_object' => NULL),
+      'render element' => 'html',
     ),
     'page' => array(
       'render element' => 'page',
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
deleted file mode 100644
index ab9aa8e..0000000
diff --git a/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
new file mode 100644
index 0000000..55fc292
--- /dev/null
+++ b/core/lib/Drupal/Core/Block/MainContentBlockPluginInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Block\MainContentBlockPluginInterface.
+ */
+
+namespace Drupal\Core\Block;
+
+/**
+ * The interface for "main page content" blocks.
+ *
+ * @ingroup block_api
+ */
+interface MainContentBlockPluginInterface extends BlockPluginInterface {
+
+  /**
+   * Sets the main content render array.
+   *
+   * @param array $main_content
+   *   The render array representing the main content.
+   */
+  public function setMainContent(array $main_content);
+
+}
diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php
index 857f9d5..e98fb67 100644
--- a/core/lib/Drupal/Core/ContentNegotiation.php
+++ b/core/lib/Drupal/Core/ContentNegotiation.php
@@ -14,6 +14,7 @@
  *
  * @todo Replace this class with a real content negotiation library based on
  *   mod_negotiation. Development of that is a work in progress.
+ *   https://www.drupal.org/node/1505080
  */
 class ContentNegotiation {
 
diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php
index c63360c..0dda746 100644
--- a/core/lib/Drupal/Core/Controller/AjaxController.php
+++ b/core/lib/Drupal/Core/Controller/AjaxController.php
@@ -7,84 +7,80 @@
 
 namespace Drupal\Core\Controller;
 
-use Drupal\Core\Ajax\AjaxResponseRenderer;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Ajax\InsertCommand;
+use Drupal\Core\Ajax\PrependCommand;
+use Drupal\Core\Render\ElementInfoManagerInterface;
 
 /**
  * Default controller for Ajax requests.
  */
-class AjaxController implements ContainerAwareInterface {
-
-  use ContainerAwareTrait;
-
-  /**
-   * The controller resolver.
-   *
-   * @var \Drupal\Core\Controller\ControllerResolverInterface
-   */
-  protected $controllerResolver;
+class AjaxController extends MainContentControllerBase {
 
   /**
-   * The Ajax response renderer.
+   * The element info manager.
    *
-   * @var \Drupal\Core\Ajax\AjaxResponseRenderer
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface
    */
-  protected $ajaxRenderer;
+  protected $elementInfoManager;
 
   /**
    * Constructs a new AjaxController instance.
    *
    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
    *   The controller resolver.
-   * @param \Drupal\Core\Ajax\AjaxResponseRenderer $ajax_renderer
-   *   The Ajax response renderer.
+   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager
+   *   The element info manager.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver, AjaxResponseRenderer $ajax_renderer) {
-    $this->controllerResolver = $controller_resolver;
-    $this->ajaxRenderer = $ajax_renderer;
+  public function __construct(ControllerResolverInterface $controller_resolver, ElementInfoManagerInterface $element_info_manager) {
+    parent::__construct($controller_resolver);
+    $this->elementInfoManager = $element_info_manager;
   }
 
   /**
-   * Controller method for Ajax content.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param callable $_content
-   *   The callable that returns the content of the Ajax response.
-   *
-   * @return \Drupal\Core\Ajax\AjaxResponse
-   *   A response object.
+   * {@inheritdoc}
    */
-  public function content(Request $request, $_content) {
-    $content = $this->getContentResult($request, $_content);
-    return $this->ajaxRenderer->render($content);
+  public function renderContentIntoResponse(array $main_content, $title, array $custom) {
+    $response = new AjaxResponse();
+
+    if (isset($main_content['#type']) && ($main_content['#type'] == 'ajax')) {
+      // Complex Ajax callbacks can return a result that contains an error
+      // message or a specific set of commands to send to the browser.
+      $main_content += $this->elementInfoManager->getInfo('ajax');
+      $error = $main_content['#error'];
+      if (!empty($error)) {
+        // Fall back to some default message otherwise use the specific one.
+        if (!is_string($error)) {
+          $error = 'An error occurred while handling the request: The server received invalid input.';
+        }
+        $response->addCommand(new AlertCommand($error));
+      }
+    }
+
+    $html = $this->drupalRender($main_content);
+
+    // The selector for the insert command is NULL as the new content will
+    // replace the element making the Ajax call. The default 'replaceWith'
+    // behavior can be changed with #ajax['method'].
+    $response->addCommand(new InsertCommand(NULL, $html));
+    $status_messages = array('#theme' => 'status_messages');
+    $output = $this->drupalRender($status_messages);
+    if (!empty($output)) {
+      $response->addCommand(new PrependCommand(NULL, $output));
+    }
+    return $response;
   }
 
   /**
-   * Returns the result of invoking the sub-controller.
+   * Wraps drupal_render().
    *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param mixed $controller_definition
-   *   A controller definition string, or a callable object/closure.
-   *
-   * @return mixed
-   *   The result of invoking the controller. Render arrays, strings, HtmlPage,
-   *   and HtmlFragment objects are possible.
+   * @todo: Remove as part of https://drupal.org/node/2182149
    */
-  public function getContentResult(Request $request, $controller_definition) {
-    if ($controller_definition instanceof \Closure) {
-      $callable = $controller_definition;
-    }
-    else {
-      $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition);
-    }
-    $arguments = $this->controllerResolver->getArguments($request, $callable);
-    $page_content = call_user_func_array($callable, $arguments);
-
-    return $page_content;
+  protected function drupalRender(&$elements, $is_root_call = FALSE) {
+    $output = drupal_render($elements, $is_root_call);
+    drupal_process_attached($elements);
+    return $output;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php
index 6aef7fa..655354d 100644
--- a/core/lib/Drupal/Core/Controller/DialogController.php
+++ b/core/lib/Drupal/Core/Controller/DialogController.php
@@ -9,22 +9,13 @@
 
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\OpenDialogCommand;
-use Drupal\Core\Page\HtmlPage;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
 
 /**
- * Defines a default controller for dialog requests.
+ * Default controller for dialog requests.
  */
-class DialogController {
-
-  /**
-   * The controller resolver service.
-   *
-   * @var \Drupal\Core\Controller\ControllerResolverInterface
-   */
-  protected $controllerResolver;
+class DialogController extends MainContentControllerBase {
 
   /**
    * The title resolver.
@@ -42,116 +33,53 @@ class DialogController {
    *   The title resolver.
    */
   public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver) {
-    $this->controllerResolver = $controller_resolver;
+    parent::__construct($controller_resolver);
     $this->titleResolver = $title_resolver;
   }
 
   /**
-   * Displays content in a modal dialog.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
-   *   The route match.
-   * @param mixed $_content
-   *   A controller definition string, or a callable object/closure.
-   *
-   * @return \Drupal\Core\Ajax\AjaxResponse
-   *   AjaxResponse to return the content wrapper in a modal dialog.
-   */
-  public function modal(Request $request, RouteMatchInterface $route_match, $_content) {
-    return $this->dialog($request, $route_match, $_content, TRUE);
-  }
-
-  /**
-   * Displays content in a dialog.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
-   *   The route match.
-   * @param mixed $_content
-   *   A controller definition string, or a callable object/closure.
-   * @param bool $modal
-   *   (optional) TRUE to render a modal dialog. Defaults to FALSE.
-   *
-   * @return \Drupal\Core\Ajax\AjaxResponse
-   *   AjaxResponse to return the content wrapper in a dialog.
+   * {@inheritdoc}
    */
-  public function dialog(Request $request, RouteMatchInterface $route_match, $_content, $modal = FALSE) {
-    $page_content = $this->getContentResult($request, $_content);
-
-    // Allow controllers to return a HtmlPage or a Response object directly.
-    if ($page_content instanceof HtmlPage) {
-      $page_content = $page_content->getContent();
-    }
-    if ($page_content instanceof Response) {
-      $page_content = $page_content->getContent();
-    }
-
-    // Most controllers return a render array, but some return a string.
-    if (!is_array($page_content)) {
-      $page_content = array(
-        '#markup' => $page_content,
-      );
-    }
-
-    $content = drupal_render_root($page_content);
-    drupal_process_attached($page_content);
-    $title = isset($page_content['#title']) ? $page_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
-    $response = new AjaxResponse();
-    // Fetch any modal options passed in from data-dialog-options.
+  public function prepareContent(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    // Determine the dialog options and the target for the OpenDialogCommand.
     $options = $request->request->get('dialogOptions', array());
-    // Set modal flag and re-use the modal ID.
-    if ($modal) {
-      $options['modal'] = TRUE;
-      $target = '#drupal-modal';
+    // Generate the target wrapper for the dialog.
+    if (isset($options['target'])) {
+      // If the target was nominated in the incoming options, use that.
+      $target = $options['target'];
+      // Ensure the target includes the #.
+      if (substr($target, 0, 1) != '#') {
+        $target = '#' . $target;
+      }
+      // This shouldn't be passed on to jQuery.ui.dialog.
+      unset($options['target']);
     }
     else {
-      // Generate the target wrapper for the dialog.
-      if (isset($options['target'])) {
-        // If the target was nominated in the incoming options, use that.
-        $target = $options['target'];
-        // Ensure the target includes the #.
-        if (substr($target, 0, 1) != '#') {
-          $target = '#' . $target;
-        }
-        // This shouldn't be passed on to jQuery.ui.dialog.
-        unset($options['target']);
-      }
-      else {
-        // Generate a target based on the route id.
-        $route_name = $route_match->getRouteName();
-        $target = '#' . drupal_html_id("drupal-dialog-$route_name");
-      }
+      // Generate a target based on the route id.
+      $route_name = $route_match->getRouteName();
+      $target = '#' . drupal_html_id("drupal-dialog-$route_name");
     }
-    $response->addCommand(new OpenDialogCommand($target, $title, $content, $options));
-    return $response;
+
+    return [
+      $main_content,
+      isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject()),
+      [
+        'dialog_options' => $options,
+        'target' => $target,
+      ]
+    ];
   }
 
   /**
-   * Returns the result of invoking the sub-controller.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param mixed $controller_definition
-   *   A controller definition string, or a callable object/closure.
-   *
-   * @return mixed
-   *   The result of invoking the controller. Render arrays, strings, HtmlPage,
-   *   and HtmlFragment objects are possible.
+   * {@inheritdoc}
    */
-  public function getContentResult(Request $request, $controller_definition) {
-    if ($controller_definition instanceof \Closure) {
-      $callable = $controller_definition;
-    }
-    else {
-      $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition);
-    }
-    $arguments = $this->controllerResolver->getArguments($request, $callable);
-    $page_content = call_user_func_array($callable, $arguments);
+  public function renderContentIntoResponse(array $main_content, $title, array $custom) {
+    $content = drupal_render_root($main_content);
+    drupal_process_attached($main_content);
 
-    return $page_content;
+    $response = new AjaxResponse();
+    $response->addCommand(new OpenDialogCommand($custom['target'], $title, $content, $custom['dialog_options']));
+    return $response;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Controller/HtmlController.php b/core/lib/Drupal/Core/Controller/HtmlController.php
new file mode 100644
index 0000000..79bce4a
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/HtmlController.php
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Controller\HtmlController.
+ */
+
+namespace Drupal\Core\Controller;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Display\PageVariantInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\system\Event\PageDisplayVariantSelectionEvent;
+use Drupal\system\Event\SystemEvents;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Default controller for HTML requests.
+ */
+class HtmlController extends MainContentControllerBase {
+
+  /**
+   * The title resolver.
+   *
+   * @var \Drupal\Core\Controller\TitleResolverInterface
+   */
+  protected $titleResolver;
+
+  /**
+   * The display variant manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $displayVariantManager;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Constructs a new HtmlController.
+   *
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
+   *   The title resolver.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
+   *   The display variant manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   */
+  public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher) {
+    parent::__construct($controller_resolver);
+    $this->titleResolver = $title_resolver;
+    $this->displayVariantManager = $display_variant_manager;
+    $this->eventDispatcher = $event_dispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * The HTML body: wraps the main content in #type 'page'.
+   */
+  public function prepareContent(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    // If the _content result already is #type => page, we have no work to do:
+    // the "main content" already is an entire "page" (see html.html.twig).
+    if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
+      $page = $main_content;
+    }
+    // Otherwise, render it as the main content of a #type => page, by selecting
+    // page display variant to do that and building that page display variant.
+    else {
+      // Select the page display variant to be used to render this main content,
+      // default to the built-in "simple page".
+      $event = new PageDisplayVariantSelectionEvent('simple_page');
+      $this->eventDispatcher->dispatch(SystemEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
+      $variant_id = $event->getPluginId();
+
+      // We must render the main content now already, because it might provide a
+      // title. We set its $is_root_call parameter to FALSE, to ensure
+      // #post_render_cache callbacks are not yet applied. This is essentially
+      // "pre-rendering" the main content, the "full rendering" will happen in
+      // ::renderContentIntoResponse().
+      // @todo Remove this once https://www.drupal.org/node/2359901 lands.
+      if (!empty($main_content)) {
+        drupal_render($main_content, FALSE);
+        $main_content = [
+          '#markup' => SafeMarkup::set($main_content['#markup']),
+          '#attached' => $main_content['#attached'],
+          '#cache' => ['tags' => $main_content['#cache']['tags']],
+          '#post_render_cache' => $main_content['#post_render_cache'],
+          '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL,
+        ];
+      }
+
+      // Instantiate the page display, and give it the main content.
+      $page_display = $this->displayVariantManager->createInstance($variant_id);
+      if (!$page_display instanceof PageVariantInterface) {
+        throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
+      }
+      $page_display->setMainContent($main_content);
+
+      // Generate a #type => page render array using the page display variant,
+      // the page display will build the content for the various page regions.
+      $page = array(
+        '#type' => 'page',
+      );
+      $page += $page_display->build();
+    }
+
+    // $page is now fully built. Find all non-empty page regions, and add a
+    // theme wrapper function that allows them to be consistently themed.
+    $regions = system_region_list(\Drupal::theme()->getActiveTheme()->getName());
+    foreach (array_keys($regions) as $region) {
+      if (!empty($page[$region])) {
+        $page[$region]['#theme_wrappers'][] = 'region';
+        $page[$region]['#region'] = $region;
+      }
+    }
+
+    // Allow hooks to add attachments to $page['#attached'].
+    static::invokePageAttachmentHooks($page);
+
+    // Determine the title: use the provided one if available, otherwise get it
+    // from the routing information.
+    $title = NULL;
+    if (isset($main_content['#title'])) {
+      $title = $main_content['#title'];
+    }
+    else {
+      $title = $this->titleResolver->getTitle($request, $route_match->getRouteObject());
+    }
+
+    return [$page, $title, []];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
+   */
+  public function renderContentIntoResponse(array $page, $title, array $custom) {
+    if (!isset($page['#type']) || $page['#type'] !== 'page') {
+      throw new \LogicException('Must be #type page');
+    }
+
+    $page['#title'] = $title;
+
+    // Now render the rendered page.html.twig template inside the html.html.twig
+    // template, and use the bubbled #attached metadata from $page to ensure we
+    // load all attached assets.
+    $html = [
+      '#type' => 'html',
+      'page' => $page,
+    ];
+    $html += element_info('html');
+
+    // The special page regions will appear directly in html.html.twig, not in
+    // page.html.twig, hence add them here, just before rendering html.html.twig.
+    static::buildPageTopAndBottom($html);
+
+    // The three parts of rendered markup in html.html.twig (page_top, page and
+    // page_bottom) must be rendered with drupal_render_root(), so that their
+    // #post_render_cache callbacks are executed (which may attach additional
+    // assets).
+    // html.html.twig must be able to render the final list of attached assets,
+    // and hence may not execute any #post_render_cache_callbacks (because they
+    // might add yet more assets to be attached), and therefore it must be
+    // rendered with drupal_render(), not drupal_render_root().
+    drupal_render_root($html['page']);
+    if (isset($html['page_top'])) {
+      drupal_render_root($html['page_top']);
+    }
+    if (isset($html['page_bottom'])) {
+      drupal_render_root($html['page_bottom']);
+    }
+    $content = drupal_render_root($html);
+
+    // Store the cache tags associated with this page in a X-Drupal-Cache-Tags
+    // header. Also associate the "rendered" cache tag. This allows us to
+    // invalidate the entire render cache, regardless of the cache bin.
+    $cache_tags = Cache::mergeTags(
+      isset($html['page_top']) ? $html['page_top']['#cache']['tags'] : [],
+      $html['page']['#cache']['tags'],
+      isset($html['page_bottom']) ? $html['page_bottom']['#cache']['tags'] : [],
+      ['rendered']
+    );
+
+    // Set the generator in the HTTP header.
+    list($version) = explode('.', \Drupal::VERSION, 2);
+
+    return new Response($content, 200,[
+      'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
+      'X-Generator' => 'Drupal ' . $version . ' (http://drupal.org)'
+    ]);
+  }
+
+  /**
+   * Invokes the page attachment hooks.
+   *
+   * @param array &$page
+   *   A #type 'page' render array, for which the page attachment hooks will be
+   *   invoked and to which the results will be added.
+   *
+   * @throws \LogicException
+   *
+   * @see hook_page_attachments()
+   * @see hook_page_attachments_alter()
+   */
+  public static function invokePageAttachmentHooks(array &$page) {
+    // Modules can add attachments.
+    $attachments = [];
+    foreach (\Drupal::moduleHandler()->getImplementations('page_attachments') as $module) {
+      $function = $module . '_page_attachments';
+      $function($attachments);
+    }
+    if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) {
+      throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments().');
+    }
+
+    // Modules and themes can alter page attachments.
+    \Drupal::moduleHandler()->alter('page_attachments', $attachments);
+    \Drupal::theme()->alter('page_attachments', $attachments);
+    if (array_diff(array_keys($attachments), ['#attached', '#post_render_cache']) !== []) {
+      throw new \LogicException('Only #attached and #post_render_cache may be set in hook_page_attachments_alter().');
+    }
+    if (isset($attachments['#attached'])) {
+      $page['#attached'] = $attachments['#attached'];
+    }
+    if (isset($attachments['#post_render_cache'])) {
+      $page['#post_render_cache'] = $attachments['#post_render_cache'];
+    }
+  }
+
+  /**
+   * Invokes the page top and bottom hooks.
+   *
+   * @param array &$html
+   *   A #type 'html' render array, for which the page top and bottom hooks will
+   *   be invoked, and to which the 'page_top' and 'page_bottom' children (also
+   *   render arrays) will be added (if non-empty).
+   *
+   * @throws \LogicException
+   *
+   * @see hook_page_top()
+   * @see hook_page_bottom()
+   * @see html.html.twig
+   */
+  public static function buildPageTopAndBottom(array &$html) {
+    // Modules can add render arrays to the top and bottom of the page.
+    $page_top = [];
+    $page_bottom = [];
+    foreach (\Drupal::moduleHandler()->getImplementations('page_top') as $module) {
+      $function = $module . '_page_top';
+      $function($page_top);
+    }
+    foreach (\Drupal::moduleHandler()->getImplementations('page_bottom') as $module) {
+      $function = $module . '_page_bottom';
+      $function($page_bottom);
+    }
+    if (!empty($page_top)) {
+      $html['page_top'] = $page_top;
+    }
+    if (!empty($page_bottom)) {
+      $html['page_bottom'] = $page_bottom;
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
deleted file mode 100644
index b2a639b..0000000
diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php
deleted file mode 100644
index efc11ac..0000000
diff --git a/core/lib/Drupal/Core/Controller/MainContentControllerBase.php b/core/lib/Drupal/Core/Controller/MainContentControllerBase.php
new file mode 100644
index 0000000..64517fc
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/MainContentControllerBase.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Controller\MainContentControllerBase.
+ */
+
+namespace Drupal\Core\Controller;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines a base class for MainContentControllerInterface implementations.
+ */
+abstract class MainContentControllerBase implements MainContentControllerInterface {
+
+  /**
+   * The controller resolver service.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * Constructs a new MainContentControllerBase.
+   *
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver service.
+   */
+  public function __construct(ControllerResolverInterface $controller_resolver) {
+    $this->controllerResolver = $controller_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMainContent(Request $request, $controller_definition) {
+    if ($controller_definition instanceof \Closure) {
+      $callable = $controller_definition;
+    }
+    else {
+      $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition);
+    }
+    $arguments = $this->controllerResolver->getArguments($request, $callable);
+    $main_content = call_user_func_array($callable, $arguments);
+
+    return $main_content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareContent(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    // In this default implementation:
+    return [
+      // We return $main_content verbatim.
+      $main_content,
+      // We don't provide a title.
+      NULL,
+      // No custom options.
+      [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request, RouteMatchInterface $route_match, $_content) {
+    $main_content = $this->getMainContent($request, $_content);
+
+    // If the received content already is a response, just pass it through. This
+    // may happen e.g. when a _content callable chooses to perform a redirect.
+    if ($main_content instanceof Response) {
+      return $main_content;
+    }
+
+    if (!is_array($main_content)) {
+      throw new \LogicException('Invalid render array returned by ' . $_content . '.');
+    }
+
+    list($content, $title, $custom) = $this->prepareContent($main_content, $request, $route_match);
+    return $this->renderContentIntoResponse($content, $title, $custom);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/MainContentControllerInterface.php b/core/lib/Drupal/Core/Controller/MainContentControllerInterface.php
new file mode 100644
index 0000000..02859f5
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/MainContentControllerInterface.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Controller\MainContentControllerInterface.
+ */
+
+namespace Drupal\Core\Controller;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * The interface for "main content" (@code _content @endcode) controllers.
+ *
+ * Controllers implementing this interface are able to render the main content
+ * (as received from "_content" controllers) in a certain format (HTML, JSON …)
+ * and/or in a certain decorated manner (e.g. in the case of the default HTML
+ * main content controller: with a page display variant applied).
+ *
+ * The three steps of handling a request for a main content controller are part
+ * of the interface:
+ * 1. getMainContent(): get the main content from the (_content) sub-controller
+ * 2. prepareContent(): apply any preparations/transformations
+ * 3. renderContentIntoResponse(): turn the content render array into a response
+ */
+interface MainContentControllerInterface  {
+
+  /**
+   * Gets the main content render array, by invoking the sub-controller.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param mixed $controller_definition
+   *   A controller definition string, or a callable object/closure.
+   *
+   * @return array|\Symfony\Component\HttpFoundation\Response
+   *   The render array representing the main content, or a Response that must
+   *   be used as the main content (e.g. when "the main content" is to redirect
+   *   the user to another location).
+   */
+  public function getMainContent(Request $request, $controller_definition);
+
+  /**
+   * Given the main content, prepare the "actual content" render array.
+   *
+   * @param array $main_content
+   *   The render array representing the main content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object, for context.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match, for context.
+   *
+   * @return array
+   *   An array with three values:
+   *   0. The prepared render array representing the (actual) content.
+   *   1. The title.
+   *   2. Key-value pairs with custom options for this main content controller.
+   */
+  public function prepareContent(array $main_content, Request $request, RouteMatchInterface $route_match);
+
+  /**
+   * Renders the content array into a response.
+   *
+   * @param array $content
+   *   The render array representing the content.
+   * @param string|NULL $title
+   *   The title of the main content, if any.
+   * @param array $custom
+   *   The optional custom options for this main content controller.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The Response in the format that this implementation supports.
+   */
+  public function renderContentIntoResponse(array $content, $title, array $custom);
+
+  /**
+   * Gets the main content render array and renders it into a response.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param mixed $controller_definition
+   *   A controller definition string, or a callable object/closure.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The Response in the format that this implementation supports.
+   */
+  public function handle(Request $request, RouteMatchInterface $route_match, $controller_definition);
+
+}
diff --git a/core/lib/Drupal/Core/Controller/ModalController.php b/core/lib/Drupal/Core/Controller/ModalController.php
new file mode 100644
index 0000000..ed1adbd
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/ModalController.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Controller\ModalController.
+ */
+
+namespace Drupal\Core\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\OpenModalDialogCommand;
+
+/**
+ * Default controller for modal dialog requests.
+ */
+class ModalController extends DialogController {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderContentIntoResponse(array $main_content, $title, array $custom) {
+    $content = drupal_render_root($main_content);
+    drupal_process_attached($main_content);
+
+    $response = new AjaxResponse();
+    $response->addCommand(new OpenModalDialogCommand($title, $content, $custom['dialog_options']));
+    return $response;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index db54f91..1f2c7b8 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -20,6 +20,7 @@
 use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass;
 use Drupal\Core\Plugin\PluginManagerPass;
+use Drupal\Core\Render\MainContentControllerPass;
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
 
 /**
@@ -54,6 +55,8 @@ public function register(ContainerBuilder $container) {
 
     $container->addCompilerPass(new StackedKernelPass());
 
+    $container->addCompilerPass(new MainContentControllerPass());
+
     // Collect tagged handler services as method calls on consumer services.
     $container->addCompilerPass(new TaggedHandlersPass());
     $container->addCompilerPass(new RegisterStreamWrappersPass());
diff --git a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php
index dd5a132..df1b52a 100644
--- a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php
+++ b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php
@@ -13,9 +13,7 @@
  * Defines a display variant annotation object.
  *
  * Display variants are used to dictate the output of a given Display, which
- * can be used to control the output of many parts of Drupal. For example, the
- * FullPageVariant is used by the Block module to control regions and output
- * block content placed in those regions.
+ * can be used to control the output of many parts of Drupal.
  *
  * Variants are usually chosen by some selection criteria, and are instantiated
  * directly. Each variant must define its own approach to rendering, and can
@@ -27,12 +25,15 @@
  *
  * Plugin namespace: Plugin\DisplayVariant
  *
- * For a working example, see
- * \Drupal\block\Plugin\DisplayVariant\FullPageVariant
+ * For working examples, see
+ * - \Drupal\system\Plugin\DisplayVariant\SimplePageVariant
+ * - \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
+ * - \Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant
  *
  * @see \Drupal\Core\Display\VariantInterface
  * @see \Drupal\Core\Display\VariantBase
  * @see \Drupal\Core\Display\VariantManager
+ * @see \Drupal\Core\Display\PageVariantInterface
  * @see plugin_api
  *
  * @Annotation
diff --git a/core/lib/Drupal/Core/Display/Annotation/PageDisplayVariant.php b/core/lib/Drupal/Core/Display/Annotation/PageDisplayVariant.php
new file mode 100644
index 0000000..bc07297
--- /dev/null
+++ b/core/lib/Drupal/Core/Display/Annotation/PageDisplayVariant.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Display\Annotation\PageDisplayVariant.
+ */
+
+namespace Drupal\Core\Display\Annotation;
+
+/**
+ * Defines a page display variant annotation object.
+ *
+ * Page display variants are a specific type of display variant, intended to
+ * render the main content of a page.
+ *
+ * @see \Drupal\Core\Display\VariantInterface
+ * @see \Drupal\Core\Display\PageVariantInterface
+ * @see \Drupal\Core\Display\VariantBase
+ * @see \Drupal\Core\Display\VariantManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class PageDisplayVariant extends DisplayVariant {
+
+}
diff --git a/core/lib/Drupal/Core/Display/PageVariantInterface.php b/core/lib/Drupal/Core/Display/PageVariantInterface.php
new file mode 100644
index 0000000..9c9f2f4
--- /dev/null
+++ b/core/lib/Drupal/Core/Display/PageVariantInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Display\PageVariantInterface.
+ */
+
+namespace Drupal\Core\Display;
+
+/**
+ * Provides an interface for PageDisplayVariant plugins.
+ *
+ * @see \Drupal\Core\Display\Annotation\DisplayVariant
+ * @see \Drupal\Core\Display\VariantBase
+ * @see \Drupal\Core\Display\VariantManager
+ * @see plugin_api
+ */
+interface PageVariantInterface extends VariantInterface {
+
+  /**
+   * Sets the main content for the page being rendered.
+   *
+   * @param array $main_content
+   *   The render array representing the main content.
+   *
+   * @return $this
+   */
+  public function setMainContent(array $main_content);
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
index 377ae93..a1f13ee 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
@@ -13,7 +13,28 @@
 use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
- * Defines a subscriber for setting the format of the request.
+ * Defines a subscriber to negotiate a _controller to use for a _content route.
+ *
+ * Given a route with a _content callback defined that returns "the content" (as
+ * a render array) to be returned at that route, this subscriber will determine
+ * which controller should render "the content" into an actual response (and set
+ * the _controller attribute on the the request).
+ *
+ * To do that, it will first determine which format to render "the content" in:
+ * that can be any of the MIME types that a render array can be transformed into.
+ *
+ * Additional target rendering formats can be defined by adding another service
+ * that implements \Drupal\Core\Controller\MainContentControllerInterface and
+ * tagging it as a @code main_content_controller @endcode, then
+ * \Drupal\Core\Render\MainContentControllerPass will detect it and use it when
+ * appropriate.
+ *
+ * Note: this also applies to routes that use _entity_view or _entity_form, for
+ * example, because those are "enhanced" into _content routes.
+ *
+ * @see \Drupal\Core\Controller\MainContentControllerInterface
+ * @see \Drupal\Core\Controller\MainContentControllerBase
+ * @see \Drupal\Core\Render\MainContentControllerPass
  */
 class ContentControllerSubscriber implements EventSubscriberInterface {
 
@@ -25,28 +46,26 @@ class ContentControllerSubscriber implements EventSubscriberInterface {
   protected $negotiation;
 
   /**
+   * The available main content controller services, keyed per format.
+   *
+   * @var array
+   */
+  protected $mainContentControllers;
+
+  /**
    * Constructs a new ContentControllerSubscriber object.
    *
    * @param \Drupal\Core\ContentNegotiation $negotiation
    *   The Content Negotiation service.
+   * @param array $main_content_controllers
+   *   The available main content controller services, keyed per format.
    */
-  public function __construct(ContentNegotiation $negotiation) {
+  public function __construct(ContentNegotiation $negotiation, array $main_content_controllers) {
     $this->negotiation = $negotiation;
+    $this->mainContentControllers = $main_content_controllers;
   }
 
   /**
-   * Associative array of supported mime types and their appropriate controller.
-   *
-   * @var array
-   */
-  protected $types = array(
-    'drupal_dialog' => 'controller.dialog:dialog',
-    'drupal_modal' => 'controller.dialog:modal',
-    'html' => 'controller.page:content',
-    'drupal_ajax' => 'controller.ajax:content',
-  );
-
-  /**
    * Sets the derived request format on the request.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
@@ -61,18 +80,21 @@ public function onRequestDeriveFormat(GetResponseEvent $event) {
   }
 
   /**
-   * Sets the _controller on a request based on the request format.
+   * Sets the derived _controller on the request, based on the request format.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    *   The event to process.
    */
-  public function onRequestDeriveContentWrapper(GetResponseEvent $event) {
+  public function onRequestDeriveController(GetResponseEvent $event) {
     $request = $event->getRequest();
 
     $controller = $request->attributes->get('_controller');
-    if (empty($controller) && ($type = $request->getRequestFormat())) {
-      if (isset($this->types[$type])) {
-        $request->attributes->set('_controller', $this->types[$type]);
+    if (empty($controller) && ($format = $request->getRequestFormat())) {
+      if (isset($this->mainContentControllers[$format])) {
+        $controller = $this->mainContentControllers[$format];
+        // MainContentControllerInterface dictates the method for handling a
+        // request is named ::handle(). Use 'service:method' notation.
+        $request->attributes->set('_controller',  $controller . ':handle');
       }
     }
   }
@@ -85,7 +107,7 @@ public function onRequestDeriveContentWrapper(GetResponseEvent $event) {
    */
   static function getSubscribedEvents() {
     $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormat', 31);
-    $events[KernelEvents::REQUEST][] = array('onRequestDeriveContentWrapper', 30);
+    $events[KernelEvents::REQUEST][] = array('onRequestDeriveController', 30);
 
     return $events;
   }
diff --git a/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php
index ae3d944..eb054a0 100644
--- a/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php
@@ -19,7 +19,7 @@
 /**
  * Exception subscriber for handling core custom error pages.
  */
-class CustomPageExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
+class CustomPageExceptionHtmlSubscriber extends DefaultExceptionHtmlSubscriber {
 
   /**
    * The configuration factory.
@@ -36,13 +36,6 @@ class CustomPageExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
   protected $aliasManager;
 
   /**
-   * The HTTP kernel.
-   *
-   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
-   */
-  protected $httpKernel;
-
-  /**
    * The logger instance.
    *
    * @var \Psr\Log\LoggerInterface
@@ -62,9 +55,9 @@ class CustomPageExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
    *   The logger service.
    */
   public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, HttpKernelInterface $http_kernel, LoggerInterface $logger) {
+    parent::__construct($http_kernel);
     $this->configFactory = $config_factory;
     $this->aliasManager = $alias_manager;
-    $this->httpKernel = $http_kernel;
     $this->logger = $logger;
   }
 
@@ -76,17 +69,7 @@ protected static function getPriority() {
   }
 
   /**
-   * {@inheritDoc}
-   */
-  protected function getHandledFormats() {
-    return ['html'];
-  }
-
-  /**
-   * Handles a 403 error for HTML.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
-   *   The event to process.
+   * {@inheritdoc}
    */
   public function on403(GetResponseForExceptionEvent $event) {
     $path = $this->aliasManager->getPathByAlias($this->configFactory->get('system.site')->get('page.403'));
@@ -94,10 +77,7 @@ public function on403(GetResponseForExceptionEvent $event) {
   }
 
   /**
-   * Handles a 404 error for HTML.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
-   *   The event to process.
+   * {@inheritdoc}
    */
   public function on404(GetResponseForExceptionEvent $event) {
     $path = $this->aliasManager->getPathByAlias($this->configFactory->get('system.site')->get('page.404'));
@@ -105,44 +85,18 @@ public function on404(GetResponseForExceptionEvent $event) {
   }
 
   /**
-   * Makes a subrequest to retrieve a custom error page.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
-   *   The event to process
-   * @param string $path
-   *   The path to which to make a subrequest for this error message.
-   * @param int $status_code
-   *   The status code for the error being handled.
+   * {@inheritdoc}
    */
   protected function makeSubrequest(GetResponseForExceptionEvent $event, $path, $status_code) {
-    $request = $event->getRequest();
-
-    // @todo Remove dependency on the internal _system_path attribute:
-    //   https://www.drupal.org/node/2293523.
-    $system_path = $request->attributes->get('_system_path');
-
-    if ($path && $path != $system_path) {
-      // @todo The create() method expects a slash-prefixed path, but we store a
-      //   normal system path in the site_404 variable.
-      if ($request->getMethod() === 'POST') {
-        $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'POST', ['destination' => $system_path, '_exception_statuscode' => $status_code] + $request->request->all(), $request->cookies->all(), [], $request->server->all());
-      }
-      else {
-        $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'GET', $request->query->all() + ['destination' => $system_path, '_exception_statuscode' => $status_code], $request->cookies->all(), [], $request->server->all());
-      }
-
-      try {
-        $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
-        $response->setStatusCode($status_code);
-        $event->setResponse($response);
-      }
-      catch (\Exception $e) {
-        // If an error happened in the subrequest we can't do much else.
-        // Instead, just log it.  The DefaultExceptionHandler will catch the
-        // original exception and handle it normally.
-        $error = Error::decodeException($e);
-        $this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error);
-      }
+    try {
+      parent::makeSubrequest($event, $path, $status_code);
+    }
+    catch (\Exception $e) {
+      // If an error happened in the subrequest we can't do much else. Instead,
+      // just log it. The DefaultExceptionSubscriber will catch the original
+      // exception and handle it normally.
+      $error = Error::decodeException($e);
+      $this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error);
     }
   }
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index 0df6a30..cef8eb4 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -7,44 +7,31 @@
 
 namespace Drupal\Core\EventSubscriber;
 
-use Drupal\Core\Page\HtmlFragment;
-use Drupal\Core\Page\HtmlFragmentRendererInterface;
-use Drupal\Core\Page\HtmlPageRendererInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
 
 /**
- * Handle most HTTP errors for HTML.
+ * Exception subscriber for handling core default error pages.
  */
 class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
-  use StringTranslationTrait;
 
   /**
-   * The HTML fragment renderer.
+   * The HTTP kernel.
    *
-   * @var \Drupal\Core\Page\HtmlFragmentRendererInterface
+   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
    */
-  protected $fragmentRenderer;
-
-  /**
-   * The HTML page renderer.
-   *
-   * @var \Drupal\Core\Page\HtmlPageRendererInterface
-   */
-  protected $htmlPageRenderer;
+  protected $httpKernel;
 
   /**
    * Constructs a new DefaultExceptionHtmlSubscriber.
    *
-   * @param \Drupal\Core\Page\HtmlFragmentRendererInterface $fragment_renderer
-   *   The fragment renderer.
-   * @param \Drupal\Core\Page\HtmlPageRendererInterface $page_renderer
-   *   The page renderer.
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
+   *   The HTTP kernel.
    */
-  public function __construct(HtmlFragmentRendererInterface $fragment_renderer, HtmlPageRendererInterface $page_renderer) {
-    $this->fragmentRenderer = $fragment_renderer;
-    $this->htmlPageRenderer = $page_renderer;
+  public function __construct(HttpKernelInterface $http_kernel) {
+    $this->httpKernel = $http_kernel;
   }
 
   /**
@@ -70,9 +57,7 @@ protected function getHandledFormats() {
    *   The event to process.
    */
   public function on403(GetResponseForExceptionEvent $event) {
-    $response = $this->createResponse($this->t('Access denied'), $this->t('You are not authorized to access this page.'), Response::HTTP_FORBIDDEN);
-    $response->headers->set('Content-type', 'text/html');
-    $event->setResponse($response);
+    $this->makeSubrequest($event, 'system/403', Response::HTTP_FORBIDDEN);
   }
 
   /**
@@ -82,40 +67,41 @@ public function on403(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on404(GetResponseForExceptionEvent $event) {
-    $path = $event->getRequest()->getPathInfo();
-    $response = $this->createResponse($this->t('Page not found'), $this->t('The requested page "@path" could not be found.', ['@path' => $path]), Response::HTTP_NOT_FOUND);
-    $response->headers->set('Content-type', 'text/html');
-    $event->setResponse($response);
+    $this->makeSubrequest($event, 'system/404', Response::HTTP_NOT_FOUND);
   }
 
   /**
-   * Handles a 405 error for HTML.
+   * Makes a subrequest to retrieve the default error page.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
-   *   The event to process.
+   *   The event to process
+   * @param string $path
+   *   The path to which to make a subrequest for this error message.
+   * @param int $status_code
+   *   The status code for the error being handled.
    */
-  public function on405(GetResponseForExceptionEvent $event) {
-    $response = new Response('Method Not Allowed', Response::HTTP_METHOD_NOT_ALLOWED);
-    $response->headers->set('Content-type', 'text/html');
-    $event->setResponse($response);
-  }
+  protected function makeSubrequest(GetResponseForExceptionEvent $event, $path, $status_code) {
+    $request = $event->getRequest();
 
-  /**
-   * @param $title
-   *   The page title of the response.
-   * @param $body
-   *   The body of the error page.
-   * @param $response_code
-   *   The HTTP response code of the response.
-   * @return Response
-   *   An error Response object ready to return to the browser.
-   */
-  protected function createResponse($title, $body, $response_code) {
-    $fragment = new HtmlFragment($body);
-    $fragment->setTitle($title);
+    // @todo Remove dependency on the internal _system_path attribute:
+    //   https://www.drupal.org/node/2293523.
+    $system_path = $request->attributes->get('_system_path');
+
+    if ($path && $path != $system_path) {
+      if ($request->getMethod() === 'POST') {
+        $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'POST', ['destination' => $system_path, '_exception_statuscode' => $status_code] + $request->request->all(), $request->cookies->all(), [], $request->server->all());
+      }
+      else {
+        $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'GET', $request->query->all() + ['destination' => $system_path, '_exception_statuscode' => $status_code], $request->cookies->all(), [], $request->server->all());
+      }
+
+      // Persist the 'exception' attribute to the subrequest.
+      $sub_request->attributes->set('exception', $request->attributes->get('exception'));
 
-    $page = $this->fragmentRenderer->render($fragment, $response_code);
-    return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
+      $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
+      $response->setStatusCode($status_code);
+      $event->setResponse($response);
+    }
   }
 
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
index e35eff4..b8d23c4 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
@@ -11,10 +11,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\ContentNegotiation;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
-use Drupal\Core\Page\HtmlFragment;
-use Drupal\Core\Page\HtmlFragmentRendererInterface;
-use Drupal\Core\Page\HtmlPageRendererInterface;
+use Drupal\Core\Render\BareHtmlPageRendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Utility\Error;
 use Symfony\Component\Debug\Exception\FlattenException;
@@ -36,20 +33,6 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
   use StringTranslationTrait;
 
   /**
-   * The fragment renderer.
-   *
-   * @var \Drupal\Core\Page\HtmlFragmentRendererInterface
-   */
-  protected $fragmentRenderer;
-
-  /**
-   * The page renderer.
-   *
-   * @var \Drupal\Core\Page\HtmlPageRendererInterface
-   */
-  protected $htmlPageRenderer;
-
-  /**
    * @var string
    *
    * One of the error level constants defined in bootstrap.inc.
@@ -64,19 +47,23 @@ class DefaultExceptionSubscriber implements EventSubscriberInterface {
   protected $configFactory;
 
   /**
+   * The bare HTML page renderer.
+   *
+   * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
+   */
+  protected $bareHtmlPageRenderer;
+
+  /**
    * Constructs a new DefaultExceptionHtmlSubscriber.
    *
-   * @param \Drupal\Core\Page\HtmlFragmentRendererInterface $fragment_renderer
-   *   The fragment renderer.
-   * @param \Drupal\Core\Page\HtmlPageRendererInterface $page_renderer
-   *   The page renderer.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration factory.
+   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
+   *   The bare HTML page renderer.
    */
-  public function __construct(HtmlFragmentRendererInterface $fragment_renderer, HtmlPageRendererInterface $page_renderer, ConfigFactoryInterface $config_factory) {
-    $this->fragmentRenderer = $fragment_renderer;
-    $this->htmlPageRenderer = $page_renderer;
+  public function __construct(ConfigFactoryInterface $config_factory, BareHtmlPageRendererInterface $bare_html_page_renderer) {
     $this->configFactory = $config_factory;
+    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
   }
 
   /**
@@ -145,7 +132,7 @@ protected function onHtml(GetResponseForExceptionEvent $event) {
     }
 
     $content = $this->t('The website has encountered an error. Please try again later.');
-    $output = DefaultHtmlPageRenderer::renderPage($content, $this->t('Error'));
+    $output = $this->bareHtmlPageRenderer->renderMaintenancePage($content, $this->t('Error'));
     $response = new Response($output);
 
     if ($exception instanceof HttpExceptionInterface) {
@@ -186,26 +173,6 @@ protected function onJson(GetResponseForExceptionEvent $event) {
   }
 
   /**
-   * Creates an Html response for the provided criteria.
-   *
-   * @param $title
-   *   The page title of the response.
-   * @param $body
-   *   The body of the error page.
-   * @param $response_code
-   *   The HTTP response code of the response.
-   * @return \Symfony\Component\HttpFoundation\Response
-   *   An error Response object ready to return to the browser.
-   */
-  protected function createHtmlResponse($title, $body, $response_code) {
-    $fragment = new HtmlFragment($body);
-    $fragment->setTitle($title);
-
-    $page = $this->fragmentRenderer->render($fragment, $response_code);
-    return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
-  }
-
-  /**
    * Handles errors for this subscriber.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
deleted file mode 100644
index b51e491..0000000
diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
index f09e3e3..164dec0 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
@@ -10,7 +10,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
+use Drupal\Core\Render\BareHtmlPageRendererInterface;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -58,6 +58,13 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
   protected $urlGenerator;
 
   /**
+   * The bare HTML page renderer.
+   *
+   * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
+   */
+  protected $bareHtmlPageRenderer;
+
+  /**
    * Constructs a new MaintenanceModeSubscriber.
    *
    * @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode
@@ -70,13 +77,16 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
    *   The url generator.
    * @param \Drupal\Core\Session\AccountInterface $account
    *   The current user.
+   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
+   *   The bare HTML page renderer.
    */
-  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account) {
+  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
     $this->maintenanceMode = $maintenance_mode;
     $this->config = $config_factory;
     $this->stringTranslation = $translation;
     $this->urlGenerator = $url_generator;
     $this->account = $account;
+    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
   }
 
   /**
@@ -95,11 +105,8 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) {
         $content = Xss::filterAdmin(String::format($this->config->get('system.maintenance')->get('message'), array(
           '@site' => $this->config->get('system.site')->get('name'),
         )));
-        // @todo Break the dependency on DefaultHtmlPageRenderer, see:
-        //   https://www.drupal.org/node/2295609
-        $content = DefaultHtmlPageRenderer::renderPage($content, $this->t('Site under maintenance'));
-        $response = new Response('Service unavailable', 503);
-        $response->setContent($content);
+        $output = $this->bareHtmlPageRenderer->renderMaintenancePage($content, $this->t('Site under maintenance'));
+        $response = new Response($output, 503);
         $event->setResponse($response);
       }
       else {
diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
index 7f13698..0b0c274 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
@@ -7,9 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
-use Drupal\Core\Ajax\AjaxResponseRenderer;
 use Drupal\Core\Controller\TitleResolverInterface;
-use Drupal\Core\Page\HtmlPage;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\JsonResponse;
@@ -43,12 +41,6 @@ class ViewSubscriber implements EventSubscriberInterface {
    */
   protected $titleResolver;
 
-  /**
-   * The Ajax response renderer.
-   *
-   * @var \Drupal\Core\Ajax\AjaxResponseRenderer
-   */
-  protected $ajaxRenderer;
 
   /**
    * Constructs a new ViewSubscriber.
@@ -57,13 +49,10 @@ class ViewSubscriber implements EventSubscriberInterface {
    *   The content negotiation.
    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
    *   The title resolver.
-   * @param \Drupal\Core\Ajax\AjaxResponseRenderer $ajax_renderer
-   *   The ajax response renderer.
    */
-  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver, AjaxResponseRenderer $ajax_renderer) {
+  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver) {
     $this->negotiation = $negotiation;
     $this->titleResolver = $title_resolver;
-    $this->ajaxRenderer = $ajax_renderer;
   }
 
   /**
@@ -79,14 +68,8 @@ public function __construct(ContentNegotiation $negotiation, TitleResolverInterf
    *   The Event to process.
    */
   public function onView(GetResponseForControllerResultEvent $event) {
-
     $request = $event->getRequest();
 
-    // For a master request, we process the result and wrap it as needed.
-    // For a subrequest, all we want is the string value.  We assume that
-    // is just an HTML string from a controller, so wrap that into a response
-    // object.  The subrequest's response will get dissected and placed into
-    // the larger page as needed.
     if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
       $method = 'on' . $this->negotiation->getContentType($request);
 
@@ -97,27 +80,6 @@ public function onView(GetResponseForControllerResultEvent $event) {
         $event->setResponse(new Response('Not Acceptable', 406));
       }
     }
-    else {
-      // This is a new-style Symfony-esque subrequest, which means we assume
-      // the body is not supposed to be a complete page but just a page
-      // fragment.
-      $page_result = $event->getControllerResult();
-      if ($page_result instanceof HtmlPage || $page_result instanceof Response) {
-        return $page_result;
-      }
-      if (!is_array($page_result)) {
-        $page_result = array(
-          '#markup' => $page_result,
-        );
-      }
-
-      // If no title was returned fall back to one defined in the route.
-      if (!isset($page_result['#title'])) {
-        $page_result['#title'] = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
-      }
-
-      $event->setResponse(new Response(drupal_render_root($page_result)));
-    }
   }
 
   public function onJson(GetResponseForControllerResultEvent $event) {
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
deleted file mode 100644
index 0cdd613..0000000
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php
deleted file mode 100644
index eea3e16..0000000
diff --git a/core/lib/Drupal/Core/Page/FeedLinkElement.php b/core/lib/Drupal/Core/Page/FeedLinkElement.php
deleted file mode 100644
index 648963b..0000000
diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php
deleted file mode 100644
index 49c421d..0000000
diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php
deleted file mode 100644
index a4e0e29..0000000
diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentInterface.php b/core/lib/Drupal/Core/Page/HtmlFragmentInterface.php
deleted file mode 100644
index f9c481b..0000000
diff --git a/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php b/core/lib/Drupal/Core/Page/HtmlFragmentRendererInterface.php
deleted file mode 100644
index 32e0076..0000000
diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php
deleted file mode 100644
index e6c75f2..0000000
diff --git a/core/lib/Drupal/Core/Page/HtmlPageRendererInterface.php b/core/lib/Drupal/Core/Page/HtmlPageRendererInterface.php
deleted file mode 100644
index dea7f64..0000000
diff --git a/core/lib/Drupal/Core/Page/LinkElement.php b/core/lib/Drupal/Core/Page/LinkElement.php
deleted file mode 100644
index 04cb2a0..0000000
diff --git a/core/lib/Drupal/Core/Page/MetaElement.php b/core/lib/Drupal/Core/Page/MetaElement.php
deleted file mode 100644
index d849ed9..0000000
diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php b/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php
deleted file mode 100644
index bc88bc3..0000000
diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php b/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php
deleted file mode 100644
index 4807be9..0000000
diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
new file mode 100644
index 0000000..5ec957e
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BareHtmlPageRenderer.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Default bare HTML page renderer
+ */
+class BareHtmlPageRenderer implements BareHtmlPageRendererInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderMaintenancePage($content, $title, array $page_additions = []) {
+    if (!is_array($content)) {
+      $content = ['#markup' => $content];
+    }
+    $attributes = [
+      'class' => [
+        'maintenance-page',
+      ],
+    ];
+    return $this->renderBarePage($content, $title, $page_additions, $attributes, 'maintenance_page');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderInstallPage($content, $title, array $page_additions = []) {
+    $attributes = [
+      'class' => [
+        'install-page',
+      ],
+    ];
+    return $this->renderBarePage($content, $title, $page_additions, $attributes, 'install_page');
+  }
+
+  /**
+   * Renders a bare page.
+   *
+   * @param string|array $content
+   *   The main content to render in the 'content' region.
+   * @param string $title
+   *   The title for this maintenance page.
+   * @param array $page_additions
+   *   Additional regions to add to the page. May also be used to pass the
+   *   #show_messages property for #type 'page'.
+   * @param array $attributes
+   *   Attributes to set on #type 'html'.
+   * @param string $page_theme_property
+   *   The #theme property to set on #type 'page'.
+   *
+   * @return string
+   *   The rendered HTML page.
+   */
+  protected function renderBarePage(array $content, $title, array $page_additions, array $attributes, $page_theme_property) {
+    $html = [
+      '#type' => 'html',
+      '#attributes' => $attributes,
+      'page' => [
+        '#type' => 'page',
+        '#theme' => $page_theme_property,
+        '#title' => $title,
+        'content' => $content,
+      ] + $page_additions,
+    ];
+
+    // We must first render the contents of the html.html.twig template, see
+    // \Drupal\Core\Controller\HtmlController::renderPage() for details.
+    drupal_render_root($html['page']);
+    return drupal_render($html);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php
new file mode 100644
index 0000000..5c9ddc2
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\BareHtmlPageRendererInterface.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Bare HTML page renderer.
+ *
+ * By "bare HTML page", we mean that the following hooks that allow for "normal"
+ * pages are not invoked:
+ * - hook_page_attachments()
+ * - hook_page_attachments_alter()
+ * - hook_page_top()
+ * - hook_page_bottom()
+ *
+ * Examples of bare HTML pages are:
+ * - install.php
+ * - update.php
+ * - authorize.php
+ * - maintenance mode
+ * - exception handlers
+ *
+ * i.e. use this when rendering HTML pages in limited environments. Otherwise,
+ * use a @code _content @endcode route, thiswill cause a main content controller
+ * (\Drupal\Core\ControllerMainContentControllerInterface) to be used, and in
+ * case of a HTML request that will be \Drupal\Core\Controller\HtmlController.
+ *
+ * Currently, there are two types of bare pages available:
+ * 1. install (hook_preprocess_install_page(), install-page.html.twig)
+ * 2. maintenance (hook_preprocess_maintenance_page(), maintenance-page.html.twig)
+ *
+ * @see \Drupal\Core\Controller\HtmlController
+ */
+interface BareHtmlPageRendererInterface {
+
+  /**
+   * Renders a "maintenance" page, styled as such.
+   *
+   * @param string|array $content
+   *   The main content to render in the 'content' region.
+   * @param string $title
+   *   The title for this maintenance page.
+   * @param array $page_additions
+   *   Additional regions to add to the page. May also be used to pass the
+   *   #show_messages property for #type 'page'.
+   *
+   * @return string
+   *   The rendered HTML page.
+   */
+  public function renderMaintenancePage($content, $title, array $page_additions = []);
+
+  /**
+   * Renders an "install" page, styled as such.
+   *
+   * @param string|array $content
+   *   The main content to render in the 'content' region.
+   * @param string $title
+   *   The title for this maintenance page.
+   * @param array $page_additions
+   *   Additional regions to add to the page. May also be used to pass the
+   *   #show_messages property for #type 'page'.
+   *
+   * @return string
+   *   The rendered HTML page.
+   */
+  public function renderInstallPage($content, $title, array $page_additions = []);
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Html.php b/core/lib/Drupal/Core/Render/Element/Html.php
index 2c4a6a0..a9b4323 100644
--- a/core/lib/Drupal/Core/Render/Element/Html.php
+++ b/core/lib/Drupal/Core/Render/Element/Html.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Component\Utility\UrlHelper;
+
 /**
- * Provides a render element for <html>.
+ * Provides a render element for an entire HTML page: <html> plus its children.
  *
  * @RenderElement("html")
  */
@@ -18,8 +20,12 @@ class Html extends RenderElement {
    * {@inheritdoc}
    */
   public function getInfo() {
+    $class = get_class($this);
     return array(
       '#theme' => 'html',
+      '#pre_render' => array(
+        array($class, 'preRenderHtml'),
+      ),
       // HTML5 Shiv
       '#attached' => array(
         'library' => array('core/html5shiv'),
@@ -27,4 +33,103 @@ public function getInfo() {
     );
   }
 
+  /**
+   * #pre_render callback for the html element type.
+   *
+   * @param array $element
+   *   A structured array containing the html element type build properties.
+   *
+   * @return array
+   *   The processed element.
+   */
+  public static function preRenderHtml(array $element) {
+    // Attach libraries and CSS used by this theme.
+    $active_theme = \Drupal::theme()->getActiveTheme();
+    foreach ($active_theme->getLibraries() as $library) {
+      $element['#attached']['library'][] = $library;
+    }
+    foreach ($active_theme->getStyleSheets() as $media => $stylesheets) {
+      foreach ($stylesheets as $stylesheet) {
+        $element['#attached']['css'][$stylesheet] = array(
+          'group' => CSS_AGGREGATE_THEME,
+          'every_page' => TRUE,
+          'media' => $media
+        );
+      }
+    }
+
+    // Attach favicon.
+    if (static::themeGetSetting('features.favicon')) {
+      $favicon = static::themeGetSetting('favicon.url');
+      $type = static::themeGetSetting('favicon.mimetype');
+      $element['#attached']['html_head_link'][][] = array(
+        'rel' => 'shortcut icon',
+        'href' => UrlHelper::stripDangerousProtocols($favicon),
+        'type' => $type,
+      );
+    }
+
+    // Get the major Drupal version.
+    list($version, ) = explode('.', \Drupal::VERSION);
+
+    // Attach default meta tags.
+    $meta_default = array(
+      // Make sure the Content-Type comes first because the IE browser may be
+      // vulnerable to XSS via encoding attacks from any content that comes
+      // before this META tag, such as a TITLE tag.
+      'system_meta_content_type' => array(
+        '#tag' => 'meta',
+        '#attributes' => array(
+          'name' => 'charset',
+          'charset' => 'utf-8',
+        ),
+        // Security: This always has to be output first.
+        '#weight' => -1000,
+      ),
+      // Show Drupal and the major version number in the META GENERATOR tag.
+      'system_meta_generator' => array(
+        '#type' => 'html_tag',
+        '#tag' => 'meta',
+        '#attributes' => array(
+          'name' => 'Generator',
+          'content' => 'Drupal ' . $version . ' (http://drupal.org)',
+        ),
+      ),
+      // Attach default mobile meta tags for responsive design.
+      'MobileOptimized' => array(
+        '#tag' => 'meta',
+        '#attributes' => array(
+          'name' => 'MobileOptimized',
+          'content' => 'width',
+        ),
+      ),
+      'HandheldFriendly' => array(
+        '#tag' => 'meta',
+        '#attributes' => array(
+          'name' => 'HandheldFriendly',
+          'content' => 'true',
+        ),
+      ),
+      'viewport' => array(
+        '#tag' => 'meta',
+        '#attributes' => array(
+          'name' => 'viewport',
+          'content' => 'width=device-width, initial-scale=1.0',
+        ),
+      ),
+    );
+    foreach ($meta_default as $key => $value) {
+      $element['#attached']['html_head'][] = [$value, $key];
+    }
+
+    return $element;
+  }
+
+  /**
+   * Wraps theme_get_setting().
+   */
+  protected static function themeGetSetting($setting_name) {
+    return theme_get_setting($setting_name);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php
index cf133af..0d0393e 100644
--- a/core/lib/Drupal/Core/Render/Element/Page.php
+++ b/core/lib/Drupal/Core/Render/Element/Page.php
@@ -8,7 +8,11 @@
 namespace Drupal\Core\Render\Element;
 
 /**
- * Provides a render element for an entire HTML page.
+ * Provides a render element for the content of an HTML page.
+ *
+ * This represents the "main part" of the HTML page's body; see html.html.twig.
+ *
+ * @todo Rename this to 'body'?
  *
  * @RenderElement("page")
  */
diff --git a/core/lib/Drupal/Core/Render/MainContentControllerPass.php b/core/lib/Drupal/Core/Render/MainContentControllerPass.php
new file mode 100644
index 0000000..bce55b9
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContentControllerPass.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContentControllerPass.
+ */
+
+namespace Drupal\Core\Render;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+/**
+ * Adds main_content_controllers parameter to the container.
+ */
+class MainContentControllerPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Collects the available main content renderer services into the
+   * main_content_controllers parameter, keyed by format.
+   */
+  public function process(ContainerBuilder $container) {
+    $main_content_renderers = [];
+    foreach ($container->findTaggedServiceIds('main_content_controller') as $id => $attributes) {
+      $format = $attributes[0]['format'];
+      $main_content_renderers[$format] = $id;
+    }
+    $container->setParameter('main_content_controllers', $main_content_renderers);
+  }
+
+}
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index 4545c19..de41c44 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -125,7 +125,7 @@ public function authenticate(Request $request) {
     }
     // Always register an IP-based failed login event.
     $this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window'));
-    return NULL;
+    return [];
   }
 
   /**
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 95e1bd6..a294dc8 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -62,43 +62,22 @@ function block_theme() {
 }
 
 /**
- * Renders blocks into their regions.
- *
- * @todo Clean this up as part of https://www.drupal.org/node/2352155.
+ * Implements hook_page_top().
  */
-function _block_page_build(&$page) {
-  $theme = \Drupal::theme()->getActiveTheme()->getName();
-
-  // Fetch a list of regions for the current theme.
-  $all_regions = system_region_list($theme);
-  if (\Drupal::routeMatch()->getRouteName() != 'block.admin_demo') {
-    // Create a full page display variant, which will load blocks into their
-    // regions.
-    $page += \Drupal::service('plugin.manager.display_variant')
-      ->createInstance('full_page')
-      ->build();
-  }
-  else {
-    // Append region description if we are rendering the regions demo page.
-    $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
-    foreach ($visible_regions as $region) {
-      $description = '<div class="block-region demo-block">' . $all_regions[$region] . '</div>';
-      $page[$region]['block_description'] = array(
-        '#markup' => $description,
-        '#weight' => 15,
-      );
-    }
-    $page['page_top']['backlink'] = array(
+function block_page_top(array &$page_top) {
+  if (\Drupal::routeMatch()->getRouteName() === 'block.admin_demo') {
+    $theme = \Drupal::theme()->getActiveTheme()->getName();
+    $page_top['backlink'] = array(
       '#type' => 'link',
       '#title' => t('Exit block region demonstration'),
       '#options' => array('attributes' => array('class' => array('block-demo-backlink'))),
       '#weight' => -10,
     );
     if (\Drupal::config('system.theme')->get('default') == $theme) {
-      $page['page_top']['backlink']['#url'] = Url::fromRoute('block.admin_display');
+      $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display');
     }
     else {
-      $page['page_top']['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]);
+      $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]);
     }
   }
 }
diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml
index dd38a43..9c7be32 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -3,6 +3,11 @@ services:
     class: Drupal\block\Theme\AdminDemoNegotiator
     tags:
       - { name: theme_negotiator, priority: 1000 }
+  block.page_display_variant_subscriber:
+    class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
+    arguments: ['@current_route_match']
+    tags:
+      - { name: event_subscriber }
   block.current_user_context:
     class: Drupal\block\EventSubscriber\CurrentUserContext
     arguments: ['@current_user', '@entity.manager']
diff --git a/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php
new file mode 100644
index 0000000..b6127fb
--- /dev/null
+++ b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber.
+ */
+
+namespace Drupal\block\EventSubscriber;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\system\Event\PageDisplayVariantSelectionEvent;
+use Drupal\system\Event\SystemEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Selects the block page display variant.
+ *
+ * @see \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
+ * @see \Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant
+ */
+class BlockPageDisplayVariantSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a BlockPageDisplayVariantSubscriber object.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(RouteMatchInterface $route_match) {
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * Selects the block page display variant.
+   *
+   * @param \Drupal\system\Event\PageDisplayVariantSelectionEvent $event
+   *   The event to process.
+   */
+  public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) {
+    if ($this->routeMatch->getRouteName() != 'block.admin_demo') {
+      $event->setPluginId('block_page');
+    }
+    else {
+      $event->setPluginId('block_demo_page');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[SystemEvents::SELECT_PAGE_DISPLAY_VARIANT][] = array('onSelectPageDisplayVariant');
+    return $events;
+  }
+
+}
diff --git a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
similarity index 77%
rename from core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php
rename to core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
index 7243362..8266bca 100644
--- a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php
+++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
@@ -2,11 +2,13 @@
 
 /**
  * @file
- * Contains \Drupal\block\Plugin\DisplayVariant\FullPageVariant.
+ * Contains \Drupal\block\Plugin\DisplayVariant\BlockPageVariant.
  */
 
 namespace Drupal\block\Plugin\DisplayVariant;
 
+use Drupal\Core\Block\MainContentBlockPluginInterface;
+use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@@ -16,14 +18,14 @@
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Provides a display variant that represents the full page.
+ * Provides a page display variant that decorates the main content with blocks.
  *
- * @DisplayVariant(
- *   id = "full_page",
- *   admin_label = @Translation("Full page")
+ * @PageDisplayVariant(
+ *   id = "block_page",
+ *   admin_label = @Translation("Page with blocks")
  * )
  */
-class FullPageVariant extends VariantBase implements ContainerFactoryPluginInterface {
+class BlockPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface {
 
   /**
    * The block storage.
@@ -61,7 +63,14 @@ class FullPageVariant extends VariantBase implements ContainerFactoryPluginInter
   protected $themeNegotiator;
 
   /**
-   * Constructs a new FullPageVariant.
+   * The render array representing the main page content.
+   *
+   * @var array
+   */
+  protected $mainContent;
+
+  /**
+   * Constructs a new BlockPageVariant.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.
@@ -114,13 +123,27 @@ protected function getTheme() {
   /**
    * {@inheritdoc}
    */
+  public function setMainContent(array $main_content) {
+    $this->mainContent = $main_content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function build() {
+    // Track whether a block that shows the main content is displayed or not.
+    $main_content_block_displayed = FALSE;
+
     $build = array();
     // Load all region content assigned via blocks.
     foreach ($this->getRegionAssignments() as $region => $blocks) {
       /** @var $blocks \Drupal\block\BlockInterface[] */
       foreach ($blocks as $key => $block) {
         if ($block->access('view')) {
+          if ($block->getPlugin() instanceof MainContentBlockPluginInterface) {
+            $block->getPlugin()->setMainContent($this->mainContent);
+            $main_content_block_displayed = TRUE;
+          }
           $build[$region][$key] = $this->blockViewBuilder->view($block);
         }
       }
@@ -129,6 +152,14 @@ public function build() {
         $build[$region]['#sorted'] = TRUE;
       }
     }
+
+    // If no block that shows the main content is displayed, still show the main
+    // content. Otherwise the end user will see all displayed blocks, but not
+    // the main content they came for.
+    if (!$main_content_block_displayed) {
+      $build['content']['system_main'] = $this->mainContent;
+    }
+
     return $build;
   }
 
diff --git a/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php
new file mode 100644
index 0000000..b1a450d
--- /dev/null
+++ b/core/modules/block/src/Plugin/DisplayVariant/DemoBlockPageVariant.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Plugin\DisplayVariant\DemoBlockPageVariant.
+ */
+
+namespace Drupal\block\Plugin\DisplayVariant;
+
+use Drupal\Core\Display\PageVariantInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Display\VariantBase;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a page display variant that demonstrates available block regions.
+ *
+ * @PageDisplayVariant(
+ *   id = "block_demo_page",
+ *   admin_label = @Translation("Page with demo blocks")
+ * )
+ */
+class DemoBlockPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The current theme.
+   *
+   * @var string
+   */
+  protected $theme;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The theme negotiator.
+   *
+   * @var \Drupal\Core\Theme\ThemeNegotiatorInterface
+   */
+  protected $themeNegotiator;
+
+  /**
+   * The render array representing the main page content.
+   *
+   * @var array
+   */
+  protected $mainContent;
+
+  /**
+   * Constructs a new DemoBlockPageVariant.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
+   *   The theme negotiator.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match, ThemeNegotiatorInterface $theme_negotiator) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->routeMatch = $route_match;
+    $this->themeNegotiator = $theme_negotiator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('current_route_match'),
+      $container->get('theme.negotiator')
+    );
+  }
+
+  /**
+   * Gets the current theme for this route.
+   *
+   * @return string
+   *   The current theme.
+   */
+  protected function getTheme() {
+    return $this->themeNegotiator->determineActiveTheme($this->routeMatch);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setMainContent(array $main_content) {
+    $this->mainContent = $main_content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    // @see \Drupal\block\Controller::demo()
+    $build = ['content' => $this->mainContent];
+
+    // Show descriptions in each visible region, nothing else.
+    $visible_regions = $this->getVisibleRegionNames();
+    foreach (array_keys($visible_regions) as $region) {
+      $description = '<div class="block-region demo-block">' . $visible_regions[$region] . '</div>';
+      $build[$region]['block_description'] = array(
+        '#markup' => $description,
+        '#weight' => 15,
+      );
+    }
+    return $build;
+  }
+
+  /**
+   * Returns the human-readable list of regions keyed by machine name.
+   *
+   * @return array
+   *   An array of human-readable region names keyed by machine name.
+   */
+  protected function getVisibleRegionNames() {
+    return system_region_list($this->getTheme(), REGIONS_VISIBLE);
+  }
+
+}
diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php
index 1996c9b..fce7341 100644
--- a/core/modules/block/src/Tests/BlockTest.php
+++ b/core/modules/block/src/Tests/BlockTest.php
@@ -44,12 +44,9 @@ function testBlockVisibility() {
     $this->drupalGet('');
     $this->assertText($title, 'Block was displayed on the front page.');
 
-    $this->drupalGet('user');
+    $this->drupalGet('user/' . $this->adminUser->id());
     $this->assertNoText($title, 'Block was not displayed according to block visibility rules.');
 
-    $this->drupalGet('USER/' . $this->adminUser->id());
-    $this->assertNoText($title, 'Block was not displayed according to block visibility rules regardless of path case.');
-
     // Confirm that the block is not displayed to anonymous users.
     $this->drupalLogout();
     $this->drupalGet('');
diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
similarity index 59%
rename from core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php
rename to core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
index 1cbcfe5..557d996b 100644
--- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/FullPageVariantTest.php
+++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\Tests\block\Unit\Plugin\DisplayVariant\FullPageVariantTest.
+ * Contains \Drupal\Tests\block\Unit\Plugin\DisplayVariant\BlockPageVariantTest.
  */
 
 namespace Drupal\Tests\block\Unit\Plugin\DisplayVariant;
@@ -10,10 +10,10 @@
 use Drupal\Tests\UnitTestCase;
 
 /**
- * @coversDefaultClass \Drupal\block\Plugin\DisplayVariant\FullPageVariant
+ * @coversDefaultClass \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
  * @group block
  */
-class FullPageVariantTest extends UnitTestCase {
+class BlockPageVariantTest extends UnitTestCase {
 
   /**
    * The block storage.
@@ -51,7 +51,7 @@ class FullPageVariantTest extends UnitTestCase {
    * @param array $definition
    *   The plugin definition array.
    *
-   * @return \Drupal\block\Plugin\DisplayVariant\FullPageVariant|\PHPUnit_Framework_MockObject_MockObject
+   * @return \Drupal\block\Plugin\DisplayVariant\BlockPageVariant|\PHPUnit_Framework_MockObject_MockObject
    *   A mocked display variant plugin.
    */
   public function setUpDisplayVariant($configuration = array(), $definition = array()) {
@@ -59,19 +59,84 @@ public function setUpDisplayVariant($configuration = array(), $definition = arra
     $this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface');
     $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
     $this->themeNegotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\FullPageVariant')
+    return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\BlockPageVariant')
       ->setConstructorArgs(array($configuration, 'test', $definition, $this->blockStorage, $this->blockViewBuilder, $this->routeMatch, $this->themeNegotiator))
       ->setMethods(array('getRegionNames'))
       ->getMock();
   }
 
+  public function providerBuild() {
+    $blocks_config = array(
+      'block1' => array(
+        TRUE, 'top', 0, FALSE,
+      ),
+      // Test a block without access.
+      'block2' => array(
+        FALSE, 'bottom', 0, FALSE,
+      ),
+      // Test two blocks in the same region with specific weight.
+      'block3' => array(
+        TRUE, 'bottom', 5, FALSE,
+      ),
+      'block4' => array(
+        TRUE, 'bottom', -5, FALSE,
+      ),
+      // Test a block implementing MainContentBlockPluginInterface.
+      'block5' => array(
+        TRUE, 'center', 0, TRUE,
+      ),
+    );
+
+    $test_cases = [];
+    $test_cases[] = [$blocks_config, 4,
+      [
+        'top' => [
+          'block1' => [],
+          '#sorted' => TRUE,
+        ],
+        // The main content was rendered via a block.
+        'center' => [
+          'block5' => [],
+          '#sorted' => TRUE,
+        ],
+        'bottom' => [
+          'block4' => [],
+          'block3' => [],
+          '#sorted' => TRUE,
+        ],
+      ],
+    ];
+    unset($blocks_config['block5']);
+    $test_cases[] = [$blocks_config, 3,
+      [
+        'top' => [
+          'block1' => [],
+          '#sorted' => TRUE,
+        ],
+        'bottom' => [
+          'block4' => [],
+          'block3' => [],
+          '#sorted' => TRUE,
+        ],
+        // The main content was rendered via the fallback in case there is no
+        // block rendering the main content.
+        'content' => [
+          'system_main' => ['#markup' => 'Hello kittens!'],
+        ],
+      ],
+    ];
+    return $test_cases;
+  }
+
   /**
    * Tests the building of a full page variant.
    *
    * @covers ::build
    * @covers ::getRegionAssignments
+   *
+   * @dataProvider providerBuild
    */
-  public function testBuild() {
+  public function testBuild(array $blocks_config, $visible_block_count, array $expected_render_array) {
     $theme = $this->randomMachineName();
     $display_variant = $this->setUpDisplayVariant();
     $this->themeNegotiator->expects($this->any())
@@ -82,26 +147,14 @@ public function testBuild() {
       ->method('getRegionNames')
       ->will($this->returnValue(array(
         'top' => 'Top',
+        'center' => 'Center',
         'bottom' => 'Bottom',
       )));
+    $display_variant->setMainContent(['#markup' => 'Hello kittens!']);
 
-    $blocks_config = array(
-      'block1' => array(
-        TRUE, 'top', 0,
-      ),
-      // Test a block without access.
-      'block2' => array(
-        FALSE, 'bottom', 0,
-      ),
-      // Test two blocks in the same region with specific weight.
-      'block3' => array(
-        TRUE, 'bottom', 5,
-      ),
-      'block4' => array(
-        TRUE, 'bottom', -5,
-      ),
-    );
     $blocks = array();
+    $block_plugin = $this->getMock('Drupal\Core\Block\BlockPluginInterface');
+    $main_content_block_plugin = $this->getMock('Drupal\Core\Block\MainContentBlockPluginInterface');
     foreach ($blocks_config as $block_id => $block_config) {
       $block = $this->getMock('Drupal\block\BlockInterface');
       $block->expects($this->once())
@@ -114,10 +167,13 @@ public function testBuild() {
           array('weight', $block_config[2]),
           array('status', TRUE),
         )));
+      $block->expects($this->any())
+        ->method('getPlugin')
+        ->willReturn($block_config[3] ? $main_content_block_plugin : $block_plugin);
       $blocks[$block_id] = $block;
     }
 
-    $this->blockViewBuilder->expects($this->exactly(3))
+    $this->blockViewBuilder->expects($this->exactly($visible_block_count))
       ->method('view')
       ->will($this->returnValue(array()));
     $this->blockStorage->expects($this->once())
@@ -125,18 +181,7 @@ public function testBuild() {
       ->with(array('theme' => $theme))
       ->will($this->returnValue($blocks));
 
-    $expected = array(
-      'top' => array(
-        'block1' => array(),
-        '#sorted' => TRUE,
-      ),
-      'bottom' => array(
-        'block4' => array(),
-        'block3' => array(),
-        '#sorted' => TRUE,
-      ),
-    );
-    $this->assertSame($expected, $display_variant->build());
+    $this->assertSame($expected_render_array, $display_variant->build());
   }
 
 }
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index 13c93e3..ebb7347 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -30,7 +30,7 @@ class FrontPageTest extends ViewTestBase {
    *
    * @var array
    */
-  public static $modules = array('node');
+  public static $modules = array('node', 'contextual');
 
   protected function setUp() {
     parent::setUp();
diff --git a/core/modules/rest/src/Tests/DeleteTest.php b/core/modules/rest/src/Tests/DeleteTest.php
index 402325c..6456df1 100644
--- a/core/modules/rest/src/Tests/DeleteTest.php
+++ b/core/modules/rest/src/Tests/DeleteTest.php
@@ -55,7 +55,7 @@ public function testDelete() {
       // Try to delete an entity that does not exist.
       $response = $this->httpRequest($entity_type . '/9999', 'DELETE');
       $this->assertResponse(404);
-      $this->assertText('The requested page "/' . $entity_type . '/9999" could not be found.');
+      $this->assertText('The requested page could not be found.');
 
       // Try to delete an entity without proper permissions.
       $this->drupalLogout();
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index 3768b90..5471d7e 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -1,7 +1,6 @@
 <?php
 
 use Drupal\Core\Database\Database;
-use Drupal\Core\Page\HtmlPage;
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
diff --git a/core/modules/system/src/Controller/BatchController.php b/core/modules/system/src/Controller/BatchController.php
index 4245480..31f2b54 100644
--- a/core/modules/system/src/Controller/BatchController.php
+++ b/core/modules/system/src/Controller/BatchController.php
@@ -7,11 +7,6 @@
 
 namespace Drupal\system\Controller;
 
-use Drupal\Core\Controller\TitleResolverInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Page\DefaultHtmlFragmentRenderer;
-use Drupal\Core\Page\HtmlPage;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -19,44 +14,7 @@
 /**
  * Controller routines for batch routes.
  */
-class BatchController implements ContainerInjectionInterface {
-
-  /**
-   * The fragment rendering service.
-   *
-   * @var \Drupal\Core\Page\DefaultHtmlFragmentRenderer
-   */
-  protected $fragmentRenderer;
-
-  /**
-   * The title resolver.
-   *
-   * @var \Drupal\Core\Controller\TitleResolverInterface
-   */
-  protected $titleResolver;
-
-  /**
-   * Constructs a new BatchController.
-   *
-   * @param \Drupal\Core\Page\DefaultHtmlFragmentRenderer $html_fragment_renderer
-   *   The fragment rendering service.
-   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
-   *   The title resolver.
-   */
-  public function __construct(DefaultHtmlFragmentRenderer $html_fragment_renderer, TitleResolverInterface $title_resolver) {
-    $this->fragmentRenderer = $html_fragment_renderer;
-    $this->titleResolver = $title_resolver;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('html_fragment_renderer'),
-      $container->get('title_resolver')
-    );
-  }
+class BatchController {
 
   /**
    * Returns a system batch page.
@@ -64,9 +22,8 @@ public static function create(ContainerInterface $container) {
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The current request object.
    *
-   * @return mixed
-   *   A \Symfony\Component\HttpFoundation\Response object or page element or
-   *   NULL.
+   * @return \Symfony\Component\HttpFoundation\Response|array
+   *   A \Symfony\Component\HttpFoundation\Response object or render array.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
    */
@@ -81,46 +38,13 @@ public function batchPage(Request $request) {
       return $output;
     }
     elseif (isset($output)) {
-      // Force a page without blocks or messages to
-      // display a list of collected messages later.
-      drupal_set_page_content($output);
-      $page = element_info('page');
-      $page['#show_messages'] = FALSE;
-
-      $page = $this->render($page);
-
+      $page = [
+        '#type' => 'page',
+        '#show_messages' => FALSE,
+        'content' => $output,
+      ];
       return $page;
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function render(array $output, $status_code = 200) {
-    if (!isset($output['#title'])) {
-      $output['#title'] = $this->titleResolver->getTitle(\Drupal::request(), \Drupal::routeMatch()->getRouteObject());
-    }
-    $page = new HtmlPage('', isset($output['#cache']) ? $output['#cache'] : array(), $output['#title']);
-
-    $page_array = drupal_prepare_page($output);
-
-    $page = $this->fragmentRenderer->preparePage($page, $page_array);
-
-    $page->setBodyTop(drupal_render_root($page_array['page_top']));
-    $page->setBodyBottom(drupal_render_root($page_array['page_bottom']));
-    $page->setContent(drupal_render_root($page_array));
-
-    drupal_process_attached($page_array);
-    if (isset($page_array['page_top'])) {
-      drupal_process_attached($page_array['page_top']);
-    }
-    if (isset($page_array['page_bottom'])) {
-      drupal_process_attached($page_array['page_bottom']);
-    }
-
-    $page->setStatusCode($status_code);
-
-    return $page;
-  }
-
 }
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index 7dff04a..2534301 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -12,7 +12,7 @@
 use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
+use Drupal\Core\Render\BareHtmlPageRendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\State\StateInterface;
@@ -69,6 +69,13 @@ class DbUpdateController extends ControllerBase {
   protected $entityDefinitionUpdateManager;
 
   /**
+   * The bare HTML page renderer.
+   *
+   * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
+   */
+  protected $bareHtmlPageRenderer;
+
+  /**
    * Constructs a new UpdateController.
    *
    * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
@@ -83,14 +90,17 @@ class DbUpdateController extends ControllerBase {
    *   The current user.
    * @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
    *   The entity definition update manager.
+   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
+   *   The bare HTML page renderer.
    */
-  public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager) {
+  public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, BareHtmlPageRendererInterface $bare_html_page_renderer) {
     $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->cache = $cache;
     $this->state = $state;
     $this->moduleHandler = $module_handler;
     $this->account = $account;
     $this->entityDefinitionUpdateManager = $entity_definition_update_manager;
+    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
   }
 
   /**
@@ -103,7 +113,8 @@ public static function create(ContainerInterface $container) {
       $container->get('state'),
       $container->get('module_handler'),
       $container->get('current_user'),
-      $container->get('entity.definition_update_manager')
+      $container->get('entity.definition_update_manager'),
+      $container->get('bare_html_page_renderer')
     );
   }
 
@@ -176,7 +187,7 @@ public function handle($op, Request $request) {
     }
     $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update');
 
-    return new Response(DefaultHtmlPageRenderer::renderPage($output, $title, 'maintenance', $regions));
+    return new Response($this->bareHtmlPageRenderer->renderMaintenancePage($output, $title, $regions));
   }
 
   /**
diff --git a/core/modules/system/src/Controller/Http4xxController.php b/core/modules/system/src/Controller/Http4xxController.php
new file mode 100644
index 0000000..7add79e
--- /dev/null
+++ b/core/modules/system/src/Controller/Http4xxController.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Controller\Http4xxController.
+ */
+
+namespace Drupal\system\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for default HTTP 4xx responses.
+ */
+class Http4xxController extends ControllerBase {
+
+  /**
+   * The default 403 content.
+   *
+   * @return array
+   *  A render array containing the message to display for 404 pages.
+   */
+  public function on403() {
+    return [
+      '#markup' => $this->t('You are not authorized to access this page.'),
+    ];
+  }
+
+  /**
+   * The default 404 content.
+   *
+   * @return array
+   *  A render array containing the message to display for 404 pages.
+   */
+  public function on404() {
+    return [
+      '#markup' => $this->t('The requested page could not be found.'),
+    ];
+  }
+
+}
diff --git a/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php b/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php
new file mode 100644
index 0000000..9177a18
--- /dev/null
+++ b/core/modules/system/src/Event/PageDisplayVariantSelectionEvent.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Event\PageDisplayVariantSelectionEvent.
+ */
+
+namespace Drupal\system\Event;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event fired when rendering main content, to select a page display variant.
+ *
+ * @see \Drupal\system\Event\SystemEvents::SELECT_PAGE_DISPLAY_VARIANT
+ */
+class PageDisplayVariantSelectionEvent extends Event {
+
+  /**
+   * The selected page display variant plugin ID.
+   *
+   * @var string
+   */
+  protected $pluginId;
+
+  /**
+   * Constructs the page display variant plugin selection event.
+   *
+   * @param string
+   *   The ID of the page display variant plugin to use by default.
+   */
+  public function __construct($plugin_id) {
+    $this->pluginId = $plugin_id;
+  }
+
+  /**
+   * The selected page display variant plugin ID.
+   *
+   * @param string $plugin_id
+   *   The ID of the page display variant plugin to use.
+   */
+  public function setPluginId($plugin_id) {
+    $this->pluginId = $plugin_id;
+  }
+
+  /**
+   * The selected page display variant plugin ID.
+   *
+   * @return string;
+   */
+  public function getPluginId() {
+    return $this->pluginId;
+  }
+
+}
diff --git a/core/modules/system/src/Event/SystemEvents.php b/core/modules/system/src/Event/SystemEvents.php
new file mode 100644
index 0000000..a99eb59
--- /dev/null
+++ b/core/modules/system/src/Event/SystemEvents.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Event\SystemEvents.
+ */
+
+namespace Drupal\system\Event;
+
+/**
+ * Defines events for the base system.
+ */
+final class SystemEvents {
+
+  /**
+   * Name of the event when selecting a page display variant to use.
+   *
+   * @see \Drupal\Core\Block\BlockBase::getConditionContexts()
+   * @see \Drupal\system\Event\PageDisplayVariantSelectionEvent
+   */
+  const SELECT_PAGE_DISPLAY_VARIANT= 'system.page_display_variant.select';
+
+}
diff --git a/core/modules/system/src/Plugin/Block/SystemMainBlock.php b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
index 4504081..caa1bdf 100644
--- a/core/modules/system/src/Plugin/Block/SystemMainBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Plugin\Block;
 
 use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Block\MainContentBlockPluginInterface;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -18,15 +19,27 @@
  *   admin_label = @Translation("Main page content")
  * )
  */
-class SystemMainBlock extends BlockBase {
+class SystemMainBlock extends BlockBase implements MainContentBlockPluginInterface {
+
+  /**
+   * The render array representing the main page content.
+   *
+   * @var array
+   */
+  protected $mainContent;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setMainContent(array $main_content) {
+    $this->mainContent = $main_content;
+  }
 
   /**
    * {@inheritdoc}
    */
   public function build() {
-    return array(
-      drupal_set_page_content()
-    );
+    return $this->mainContent;
   }
 
   /**
diff --git a/core/modules/system/src/Plugin/DisplayVariant/SimplePageVariant.php b/core/modules/system/src/Plugin/DisplayVariant/SimplePageVariant.php
new file mode 100644
index 0000000..afe298a
--- /dev/null
+++ b/core/modules/system/src/Plugin/DisplayVariant/SimplePageVariant.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\DisplayVariant\SimplePageVariant.
+ */
+
+namespace Drupal\system\Plugin\DisplayVariant;
+
+use Drupal\Core\Display\PageVariantInterface;
+use Drupal\Core\Display\VariantBase;
+
+/**
+ * Provides a page display variant that simply renders the main content.
+ *
+ * @PageDisplayVariant(
+ *   id = "simple_page",
+ *   admin_label = @Translation("Simple page")
+ * )
+ */
+class SimplePageVariant extends VariantBase implements PageVariantInterface {
+
+  /**
+   * The render array representing the main content.
+   *
+   * @var array
+   */
+  protected $mainContent;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setMainContent(array $main_content) {
+    $this->mainContent = $main_content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    $build = [
+      'content' => $this->mainContent,
+    ];
+    return $build;
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Common/AddFeedTest.php b/core/modules/system/src/Tests/Common/AddFeedTest.php
index ba55937..0f5a560 100644
--- a/core/modules/system/src/Tests/Common/AddFeedTest.php
+++ b/core/modules/system/src/Tests/Common/AddFeedTest.php
@@ -7,8 +7,6 @@
 
 namespace Drupal\system\Tests\Common;
 
-use Drupal\Core\Page\FeedLinkElement;
-use Drupal\Core\Page\HtmlPage;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -30,10 +28,6 @@ function testBasicFeedAddNoTitle() {
     $external_for_title = 'http://' . $this->randomMachineName(12) . '/' . $this->randomMachineName(12);
     $fully_qualified_for_title = _url($this->randomMachineName(12), array('absolute' => TRUE));
 
-    // Possible permutations of _drupal_add_feed() to test.
-    // - 'input_url': the path passed to _drupal_add_feed(),
-    // - 'output_url': the expected URL to be found in the header.
-    // - 'title' == the title of the feed as passed into _drupal_add_feed().
     $urls = array(
       'path without title' => array(
         'url' => _url($path, array('absolute' => TRUE)),
@@ -61,14 +55,14 @@ function testBasicFeedAddNoTitle() {
       ),
     );
 
-    $html_page = new HtmlPage();
-
+    $build = [];
     foreach ($urls as $feed_info) {
-      $feed_link = new FeedLinkElement($feed_info['title'], $feed_info['url']);
-      $html_page->addLinkElement($feed_link);
+      $build['#attached']['feed'][] = [$feed_info['url'], $feed_info['title']];
     }
 
-    $this->drupalSetContent(\Drupal::service('html_page_renderer')->render($html_page));
+    drupal_process_attached($build);
+
+    $this->drupalSetContent(drupal_get_html_head());
     foreach ($urls as $description => $feed_info) {
       $this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
     }
@@ -80,7 +74,7 @@ function testBasicFeedAddNoTitle() {
   function urlToRSSLinkPattern($url, $title = '') {
     // Escape any regular expression characters in the URL ('?' is the worst).
     $url = preg_replace('/([+?.*])/', '[$0]', $url);
-    $generated_pattern = '%<link +title="' . $title . '" +type="application/rss.xml" +href="' . $url . '" +rel="alternate" */>%';
+    $generated_pattern = '%<link +href="' . $url . '" +rel="alternate" +title="' . $title . '" +type="application/rss.xml" */>%';
     return $generated_pattern;
   }
 
diff --git a/core/modules/system/src/Tests/Common/PageRenderTest.php b/core/modules/system/src/Tests/Common/PageRenderTest.php
index 09dd9b3..b270170 100644
--- a/core/modules/system/src/Tests/Common/PageRenderTest.php
+++ b/core/modules/system/src/Tests/Common/PageRenderTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Common;
 
+use Drupal\Core\Controller\HtmlController;
 use Drupal\simpletest\KernelTestBase;
 
 /**
@@ -61,14 +62,14 @@ function testHookPageAttachmentsAlter() {
   function assertPageRenderHookExceptions($module, $hook) {
     // Assert a valid hook implementation doesn't trigger an exception.
     $page = [];
-    drupal_prepare_page($page);
+    HtmlController::invokePageAttachmentHooks($page);
 
     // Assert an invalid hook implementation doesn't trigger an exception.
     \Drupal::state()->set($module . '.' . $hook . '.descendant_attached', TRUE);
     $assertion = $hook . '() implementation that sets #attached on a descendant triggers an exception';
     $page = [];
     try {
-      drupal_prepare_page($page);
+      HtmlController::invokePageAttachmentHooks($page);
       $this->error($assertion);
     }
     catch (\LogicException $e) {
@@ -82,7 +83,7 @@ function assertPageRenderHookExceptions($module, $hook) {
     $assertion = $hook . '() implementation that sets a child render array triggers an exception';
     $page = [];
     try {
-      drupal_prepare_page($page);
+      HtmlController::invokePageAttachmentHooks($page);
       $this->error($assertion);
     }
     catch (\LogicException $e) {
diff --git a/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php b/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php
index b6ed513..0d925b7 100644
--- a/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php
+++ b/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php
@@ -77,14 +77,14 @@ public function testHtml403() {
 
     /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
     $kernel = \Drupal::getContainer()->get('http_kernel');
-    $response = $kernel->handle($request);
+    $response = $kernel->handle($request)->prepare($request);
 
     $this->assertEqual($response->getStatusCode(), Response::HTTP_FORBIDDEN);
-    $this->assertEqual($response->headers->get('Content-type'), 'text/html');
+    $this->assertEqual($response->headers->get('Content-type'), 'text/html; charset=UTF-8');
   }
 
   /**
-   * Tests the exception handling for HTML and 403 status code.
+   * Tests the exception handling for HTML and 404 status code.
    */
   public function testHtml404() {
     $request = Request::create('/not-found');
@@ -93,27 +93,10 @@ public function testHtml404() {
 
     /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
     $kernel = \Drupal::getContainer()->get('http_kernel');
-    $response = $kernel->handle($request);
+    $response = $kernel->handle($request)->prepare($request);
 
     $this->assertEqual($response->getStatusCode(), Response::HTTP_NOT_FOUND);
-    $this->assertEqual($response->headers->get('Content-type'), 'text/html');
-  }
-
-  /**
-   * Tests the exception handling for HTML and 405 status code.
-   */
-  public function testHtml405() {
-    $request = Request::create('/admin', 'NOT_EXISTING');
-    $request->headers->set('Accept', 'text/html');
-    $request->setFormat('html', ['text/html']);
-
-    /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
-    $kernel = \Drupal::getContainer()->get('http_kernel');
-    $response = $kernel->handle($request);
-
-    $this->assertEqual($response->getStatusCode(), Response::HTTP_METHOD_NOT_ALLOWED);
-    $this->assertEqual($response->headers->get('Content-type'), 'text/html');
-    $this->assertEqual($response->getContent(), 'Method Not Allowed');
+    $this->assertEqual($response->headers->get('Content-type'), 'text/html; charset=UTF-8');
   }
 
 }
diff --git a/core/modules/system/src/Tests/System/MainContentFallbackTest.php b/core/modules/system/src/Tests/System/MainContentFallbackTest.php
deleted file mode 100644
index 288e681..0000000
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index b285bcf..26317f6 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -7,6 +7,22 @@ system.ajax:
   requirements:
     _access: 'TRUE'
 
+system.403:
+  path: '/system/403'
+  defaults:
+    _content: '\Drupal\system\Controller\Http4xxController:on403'
+    _title: 'Access denied'
+  requirements:
+    _access: 'TRUE'
+
+system.404:
+  path: '/system/404'
+  defaults:
+    _content: '\Drupal\system\Controller\Http4xxController:on404'
+    _title: 'Page not found'
+  requirements:
+    _access: 'TRUE'
+
 system.admin:
   path: '/admin'
   defaults:
@@ -403,13 +419,13 @@ system.admin_config:
   requirements:
     _permission: 'access administration pages'
 
-# @todo system.batch_page.html does not work due to some ordering issues.
-system.batch_page.normal:
+system.batch_page.html:
   path: '/batch'
   defaults:
     _content: '\Drupal\system\Controller\BatchController::batchPage'
   requirements:
     _access: 'TRUE'
+    _format: 'html'
   options:
     _admin_route: TRUE
 
diff --git a/core/modules/system/templates/html.html.twig b/core/modules/system/templates/html.html.twig
index d605532..8e9cf40 100644
--- a/core/modules/system/templates/html.html.twig
+++ b/core/modules/system/templates/html.html.twig
@@ -29,18 +29,18 @@
 <!DOCTYPE html>
 <html{{ html_attributes }}>
   <head>
-    {{ page.head }}
+    {{ html.head }}
     <title>{{ head_title }}</title>
-    {{ page.styles }}
-    {{ page.scripts }}
+    {{ html.styles }}
+    {{ html.scripts }}
   </head>
   <body{{ attributes }}>
     <a href="#main-content" class="visually-hidden focusable skip-link">
       {{ 'Skip to main content'|t }}
     </a>
-    {{ page_top }}
-    {{ page.content }}
-    {{ page_bottom }}
-    {{ page.scripts('footer') }}
+    {{ html.page_top }}
+    {{ html.page }}
+    {{ html.page_bottom }}
+    {{ html.scripts_bottom }}
   </body>
 </html>
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
index 9a7fb8a..2ecbe2a 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
@@ -194,7 +194,7 @@ function ajax_forms_test_advanced_commands_settings_with_merging_callback($form,
 function ajax_forms_test_validation_form_callback($form, FormStateInterface $form_state) {
   drupal_set_message("ajax_forms_test_validation_form_callback invoked");
   drupal_set_message(t("Callback: drivertext=%drivertext, spare_required_field=%spare_required_field", array('%drivertext' => $form_state->getValue('drivertext'), '%spare_required_field' => $form_state->getValue('spare_required_field'))));
-  return '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>';
+  return ['#markup' => '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>'];
 }
 
 /**
@@ -203,7 +203,7 @@ function ajax_forms_test_validation_form_callback($form, FormStateInterface $for
 function ajax_forms_test_validation_number_form_callback($form, FormStateInterface $form_state) {
   drupal_set_message("ajax_forms_test_validation_number_form_callback invoked");
   drupal_set_message(t("Callback: drivernumber=%drivernumber, spare_required_field=%spare_required_field", array('%drivernumber' => $form_state->getValue('drivernumber'), '%spare_required_field' => $form_state->getValue('spare_required_field'))));
-  return '<div id="message_area_number">ajax_forms_test_validation_number_form_callback at ' . date('c') . '</div>';
+  return ['#markup' => '<div id="message_area_number">ajax_forms_test_validation_number_form_callback at ' . date('c') . '</div>'];
 }
 
 /**
diff --git a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
index 75fa1c1..00619ef 100644
--- a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
+++ b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
@@ -88,7 +88,7 @@ public function testEdit(Request $request, $entity_type_id) {
    * @see \Drupal\entity_test\Routing\EntityTestRoutes::routes()
    */
   public function testAdmin() {
-    return '';
+    return [];
   }
 
   /**
diff --git a/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php b/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php
index 7a4bf90..8ba584a 100644
--- a/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php
+++ b/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php
@@ -53,7 +53,7 @@ public function generateWarnings($collect_errors = FALSE) {
     $awesomely_big = 1/0;
     // This will generate a user error.
     trigger_error("Drupal is awesome", E_USER_WARNING);
-    return "";
+    return [];
   }
 
   /**
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index a5ce74e..a87983b 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -91,7 +91,7 @@ function menu_test_menu_local_tasks_alter(&$data, $route_name) {
  * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::menuTestCallback()
  */
 function menu_test_callback() {
-  return 'This is menu_test_callback().';
+  return ['#markup' => 'This is menu_test_callback().'];
 }
 
 /**
@@ -117,7 +117,7 @@ function menu_test_theme_page_callback($inherited = FALSE) {
   if ($inherited) {
     $output .= ' Theme negotiation inheritance is being tested.';
   }
-  return $output;
+  return ['#markup' => $output];
 }
 
 /**
diff --git a/core/modules/system/tests/modules/menu_test/src/TestControllers.php b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
index 608e0c7..bbc4f1f 100644
--- a/core/modules/system/tests/modules/menu_test/src/TestControllers.php
+++ b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
@@ -19,28 +19,28 @@ class TestControllers {
    * Returns page to be used as a login path.
    */
   public function testLogin() {
-    return 'This is TestControllers::testLogin.';
+    return ['#markup' => 'This is TestControllers::testLogin.'];
   }
 
   /**
    * Prints out test data.
    */
   public function test1() {
-    return 'test1';
+    return ['#markup' => 'test1'];
   }
 
   /**
    * Prints out test data.
    */
   public function test2() {
-    return 'test2';
+    return ['#markup' => 'test2'];
   }
 
   /**
    * Prints out test data.
    */
   public function testDerived() {
-    return 'testDerived';
+    return ['#markup' => 'testDerived'];
   }
 
   /**
@@ -54,10 +54,10 @@ public function testDerived() {
    */
   public function testDefaults($placeholder = NULL) {
     if ($placeholder) {
-      return String::format("Sometimes there is a placeholder: '@placeholder'.", array('@placeholder' => $placeholder));
+      return ['#markup' => String::format("Sometimes there is a placeholder: '@placeholder'.", array('@placeholder' => $placeholder))];
     }
     else {
-      return String::format('Sometimes there is no placeholder.');
+      return ['#markup' => String::format('Sometimes there is no placeholder.')];
     }
   }
 
diff --git a/core/modules/system/tests/modules/module_test/src/Controller/ModuleTestController.php b/core/modules/system/tests/modules/module_test/src/Controller/ModuleTestController.php
index df3b8f7..3a2532e 100644
--- a/core/modules/system/tests/modules/module_test/src/Controller/ModuleTestController.php
+++ b/core/modules/system/tests/modules/module_test/src/Controller/ModuleTestController.php
@@ -30,7 +30,7 @@ public function hookDynamicLoadingInvokeAll() {
    * @todo Remove module_test_class_loading().
    */
   public function testClassLoading() {
-    return module_test_class_loading();
+    return ['#markup' => module_test_class_loading()];
   }
 
 }
diff --git a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
index 8b48b45..33f52fc 100644
--- a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
+++ b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php
@@ -19,14 +19,14 @@ class TestControllers {
   public function testUserNodeFoo(EntityInterface $user, NodeInterface $node, Request $request) {
     $foo = $request->attributes->get('foo');
     $foo = is_object($foo) ? $foo->label() : $foo;
-    return "user: {$user->label()}, node: {$node->label()}, foo: $foo";
+    return ['#markup' => "user: {$user->label()}, node: {$node->label()}, foo: $foo"];
   }
 
   public function testNodeSetParent(NodeInterface $node, NodeInterface $parent) {
-    return "Setting '{$parent->label()}' as parent of '{$node->label()}'.";
+    return ['#markup' => "Setting '{$parent->label()}' as parent of '{$node->label()}'."];
   }
 
   public function testEntityLanguage(NodeInterface $node) {
-    return $node->label();
+    return ['#markup' => $node->label()];
   }
 }
diff --git a/core/modules/system/tests/modules/router_test_directory/src/TestContent.php b/core/modules/system/tests/modules/router_test_directory/src/TestContent.php
index 6559c70..dc2caca 100644
--- a/core/modules/system/tests/modules/router_test_directory/src/TestContent.php
+++ b/core/modules/system/tests/modules/router_test_directory/src/TestContent.php
@@ -43,7 +43,7 @@ public static function create(ContainerInterface $container) {
    * Provides example content for testing route enhancers.
    */
   public function test1() {
-    return 'abcde';
+    return ['#markup' => 'abcde'];
   }
 
   /**
@@ -54,13 +54,13 @@ public function test1() {
    */
   public function test11() {
     $account = $this->currentUser();
-    return $account->getUsername();
+    return ['#markup' => $account->getUsername()];
   }
 
   public function testAccount(UserInterface $user) {
     $current_user_name = $this->currentUser()->getUsername();
     $this->currentUser()->setAccount($user);
-    return $current_user_name . ':' . $user->getUsername();
+    return ['#markup' => $current_user_name . ':' . $user->getUsername()];
   }
 
   /**
diff --git a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
index 1eec263..10ba2ca 100644
--- a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
+++ b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
@@ -26,19 +26,19 @@ public function test1() {
   }
 
   public function test2() {
-    return "test2";
+    return ['#markup' => "test2"];
   }
 
   public function test3($value) {
-    return $value;
+    return ['#markup' => $value];
   }
 
   public function test4($value) {
-    return $value;
+    return ['#markup' => $value];
   }
 
   public function test5() {
-    return "test5";
+    return ['#markup' => "test5"];
   }
 
   public function test6() {
diff --git a/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php b/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
index 5fbd412..5993056 100644
--- a/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
+++ b/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
@@ -24,8 +24,8 @@ class SessionTestController extends ControllerBase {
    */
   public function get() {
     return empty($_SESSION['session_test_value'])
-      ? ""
-      : $this->t('The current value of the stored session variable is: %val', array('%val' => $_SESSION['session_test_value']));
+      ? []
+      : ['#markup' => $this->t('The current value of the stored session variable is: %val', array('%val' => $_SESSION['session_test_value']))];
   }
 
   /**
@@ -41,7 +41,7 @@ public function getId() {
 
     \Drupal::service('session_manager')->save();
 
-    return 'session_id:' . session_id() . "\n";
+    return ['#markup' => 'session_id:' . session_id() . "\n"];
   }
 
   /**
@@ -54,7 +54,7 @@ public function getId() {
    *   A notification message with session ID.
    */
   public function getIdFromCookie(Request $request) {
-    return 'session_id:' . $request->cookies->get(session_name()) . "\n";
+    return ['#markup' => 'session_id:' . $request->cookies->get(session_name()) . "\n"];
   }
 
   /**
@@ -69,7 +69,7 @@ public function getIdFromCookie(Request $request) {
   public function set($test_value) {
     $_SESSION['session_test_value'] = $test_value;
 
-    return $this->t('The current value of the stored session variable has been set to %val', array('%val' => $test_value));
+    return ['#markup' => $this->t('The current value of the stored session variable has been set to %val', array('%val' => $test_value))];
   }
 
   /**
@@ -85,7 +85,7 @@ public function set($test_value) {
   public function noSet($test_value) {
     \Drupal::service('session_manager')->disable();
     $this->set($test_value);
-    return $this->t('session saving was disabled, and then %val was set', array('%val' => $test_value));
+    return ['#markup' => $this->t('session saving was disabled, and then %val was set', array('%val' => $test_value))];
   }
 
   /**
@@ -111,6 +111,7 @@ public function setMessage() {
   public function setMessageButDontSave() {
     \Drupal::service('session_manager')->disable();
     $this->setMessage();
+    return ['#markup' => ''];
   }
 
   /**
@@ -124,6 +125,7 @@ public function setNotStarted() {
     if (!drupal_session_will_start()) {
       $this->set($this->t('Session was not started'));
     }
+    return ['#markup' => ''];
   }
 
   /**
@@ -133,6 +135,6 @@ public function setNotStarted() {
    *   A notification message.
    */
   public function isLoggedIn() {
-    return $this->t('User is logged in.');
+    return ['#markup' => $this->t('User is logged in.')];
   }
 }
diff --git a/core/modules/system/tests/modules/system_module_test/src/EventSubscriber/HtmlPageSubscriber.php b/core/modules/system/tests/modules/system_module_test/src/EventSubscriber/HtmlPageSubscriber.php
deleted file mode 100644
index f64cd54..0000000
diff --git a/core/modules/system/tests/modules/system_module_test/system_module_test.module b/core/modules/system/tests/modules/system_module_test/system_module_test.module
index 8d67e7a..4d5e073 100644
--- a/core/modules/system/tests/modules/system_module_test/system_module_test.module
+++ b/core/modules/system/tests/modules/system_module_test/system_module_test.module
@@ -5,3 +5,23 @@
  * Provides System module hook implementations for testing purposes.
  */
 
+/**
+ * Implements hook_element_info_alter().
+ */
+function system_module_test_element_info_alter(&$types) {
+  $types['html']['#pre_render'][] = 'system_module_test_html_pre_render';
+}
+
+/**
+ * Additional #pre_render callback for 'html' elements.
+ */
+function system_module_test_html_pre_render(array $element) {
+  // Remove the HTML5 mobile meta-tags.
+  $meta_tags_to_remove = ['MobileOptimized', 'HandheldFriendly', 'viewport', 'cleartype'];
+  foreach ($element['#attached']['html_head'] as $index => $parts) {
+    if (in_array($parts[1], $meta_tags_to_remove)) {
+      unset($element['#attached']['html_head'][$index]);
+    }
+  }
+  return $element;
+}
diff --git a/core/modules/system/tests/modules/system_module_test/system_module_test.services.yml b/core/modules/system/tests/modules/system_module_test/system_module_test.services.yml
deleted file mode 100644
index 3abeb5e..0000000
diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
index c6b6903..9906b10 100644
--- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
+++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
@@ -49,7 +49,7 @@ public static function create(ContainerInterface $container) {
    *   The text to display.
    */
   public function mainContentFallback() {
-    return $this->t('Content to test main content fallback');
+    return ['#markup' => $this->t('Content to test main content fallback')];
   }
 
   /**
@@ -65,7 +65,7 @@ public function drupalSetMessageTest() {
 
     // Remove the first.
     unset($_SESSION['messages']['status'][0]);
-    return '';
+    return [];
   }
 
   /**
@@ -93,10 +93,10 @@ public function lockExit() {
    */
   public function lockPersist($lock_name) {
     if ($this->persistentLock->acquire($lock_name)) {
-      return 'TRUE: Lock successfully acquired in SystemTestController::lockPersist()';
+      return ['#markup' => 'TRUE: Lock successfully acquired in SystemTestController::lockPersist()'];
     }
     else {
-      return 'FALSE: Lock not acquired in SystemTestController::lockPersist()';
+      return ['#markup' => 'FALSE: Lock not acquired in SystemTestController::lockPersist()'];
     }
   }
 
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index 78c1504..b44e0a0 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -72,10 +72,10 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
 function system_test_lock_acquire() {
   if (\Drupal::lock()->acquire('system_test_lock_acquire')) {
     \Drupal::lock()->release('system_test_lock_acquire');
-    return 'TRUE: Lock successfully acquired in system_test_lock_acquire()';
+    return ['#markup' => 'TRUE: Lock successfully acquired in system_test_lock_acquire()'];
   }
   else {
-    return 'FALSE: Lock not acquired in system_test_lock_acquire()';
+    return ['#markup' => 'FALSE: Lock not acquired in system_test_lock_acquire()'];
   }
 }
 
@@ -91,7 +91,7 @@ function system_test_lock_exit() {
     exit();
   }
   else {
-    return 'FALSE: Lock not acquired in system_test_lock_exit()';
+    return ['#markup' => 'FALSE: Lock not acquired in system_test_lock_exit()'];
   }
 }
 
@@ -99,19 +99,6 @@ function system_test_lock_exit() {
  * Implements hook_page_attachments().
  */
 function system_test_page_attachments(array &$page) {
-  $menu_item['path'] = current_path();
-  $main_content_display = &drupal_static('system_main_content_added', FALSE);
-
-  if ($menu_item['path'] == 'system-test/main-content-fallback') {
-    // Get the main content, to e.g. dynamically attach an asset.
-    drupal_set_page_content();
-    // Indicate we don't want to override the main content.
-    $main_content_display = FALSE;
-  }
-  elseif ($menu_item['path'] == 'system-test/main-content-handling') {
-    // Set the main content.
-    drupal_set_page_content('<div id="system-test-content">Overridden!</div>');
-  }
   // Used by FrontPageTestCase to get the results of drupal_is_front_page().
   $frontpage = \Drupal::state()->get('system_test.front_page_output') ?: 0;
   if ($frontpage && drupal_is_front_page()) {
diff --git a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php
index caf9533..fb8e1f9 100644
--- a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php
+++ b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php
@@ -56,7 +56,7 @@ public function testInfoStylesheets() {
    *   A render array containing a theme override.
    */
   public function testTemplate() {
-    return \Drupal::theme()->render('theme_test_template_test', array());
+    return ['#markup' => \Drupal::theme()->render('theme_test_template_test', array())];
   }
 
   /**
@@ -82,7 +82,7 @@ public function testInlineTemplate() {
    *   An HTML string containing the themed output.
    */
   public function testSuggestion() {
-    return \Drupal::theme()->render(array('theme_test__suggestion', 'theme_test'), array());
+    return ['#markup' => \Drupal::theme()->render(array('theme_test__suggestion', 'theme_test'), array())];
   }
 
   /**
@@ -92,7 +92,7 @@ public function testSuggestion() {
    *   Content in theme_test_output GLOBAL.
    */
   public function testRequestListener() {
-    return $GLOBALS['theme_test_output'];
+    return ['#markup' =>  $GLOBALS['theme_test_output']];
   }
 
   /**
diff --git a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
index 9441a50..f657c4d 100644
--- a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
+++ b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
@@ -18,7 +18,7 @@ class TwigThemeTestController {
    * Menu callback for testing PHP variables in a Twig template.
    */
   public function phpVariablesRender() {
-    return \Drupal::theme()->render('twig_theme_test_php_variables', array());
+    return ['#markup' => \Drupal::theme()->render('twig_theme_test_php_variables', array())];
   }
 
   /**
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index fba946f..bd52ba9 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -302,6 +302,67 @@
  *
  * See drupal_process_attached() for additional information.
  *
+ * @section render_pipeline The Render Pipeline (or: how Drupal renders pages)
+ *
+ * First, you need to know the general routing concepts: please read
+ * @ref sec_controller first.
+ *
+ * A _controller route expects the controller to return a Response.
+ *
+ * A non-_controller (typically _content) route expects the controller to return
+ * the "main content", as a render array. That main content may be requested and
+ * rendered in multiple ways: it can be rendered in a certain format (HTML, JSON
+ * …) and/or in a certain decorated manner (e.g. with blocks around the main
+ * content).
+ * Therefore the first step is to negotiate a format
+ * (\Drupal\Core\EventSubscriber\ContentControllerSubscriber::onRequestDeriveFormat())
+ * which will determine which "main content controller" (which implementation of
+ * \Drupal\Core\Controller\MainContentControllerInterface) to use. The selected
+ * main content controller will be set on the request as the _controller to use
+ * (just like a "regular" _controller route attribute).
+ *
+ * Having negotiated a _controller, we now go to the three stages of main
+ * content controllers:
+ * 1. getMainContent(): get the main content from the (_content) sub-controller,
+ *    this must always be a render array
+ * 2. prepareContent(): apply any preparations/transformations
+ * 3. renderContentIntoResponse(): turn the content render array into a response
+ *
+ * These same steps are applied regardless of which main content controller is
+ * selected: it's the same for the AJAX, HTML, Drupal Dialog and Drupal Modal
+ * Dialog controllers.
+ *
+ * Specific main content controllers may of course implement additional
+ * flexibility if they want to. The default HTML controller does this, for
+ * example. And since rendering HTML pages is the most common use for Drupal,
+ * this will cover that also.
+ *
+ * \Drupal\Core\Controller\HtmlController::prepareContent() looks at the render
+ * array it receives. If it's already #type 'page', then most of the work it
+ * should do is already done. After all, if a #type 'page' is indicated to be
+ * the main content, then that implies no decorations should be applied, since
+ * it already represents the final <body> for the HTML document.
+ * If it's not yet #type 'page', however, then we need to build that still. The
+ * SystemEvents::SELECT_PAGE_DISPLAY_VARIANT event is dispatched, to select a
+ * page display variant. By default, SimplePageVariant is used, which doesn't do
+ * any decorating. But when Block module is enabled, BlockPageVariant is used,
+ * which allows the site builder to place blocks in any of the page regions, and
+ * hence "decorate" the main content.
+ * The outcome of \Drupal\Core\Controller\HtmlController::prepareContent() is
+ * always a #type 'page' render array. This is considered the "actual" content
+ * for HTML responses.
+ *
+ * \Drupal\Core\Controller\HtmlController::renderContentIntoResponse() — the
+ * third and final step — then wraps the #type 'page' (which represents
+ * page.html.twig) in a #type 'html' render array (which represents
+ * html.html.twig), to then render the entire HTML document.
+ *
+ * For HTML pages to be rendered in limited environments, such as when you are
+ * installing or updating Drupal, or when you put it in maintenance mode, or
+ * when an error occurs, a simpler HTML page renderer is used for rendering
+ * these bare pages: \Drupal\Core\Render\BareHtmlPageRenderer
+ *
+ *
  * @see themeable
  *
  * @}
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 02e9f52..ecd2cf2 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -306,7 +306,7 @@ function views_page_display_pre_render(array $element) {
 /**
  * Implements MODULE_preprocess_HOOK().
  */
-function views_preprocess_page(&$variables) {
+function views_preprocess_html(&$variables) {
   // Early-return to prevent adding unnecessary JavaScript.
   if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
     return;
@@ -325,20 +325,8 @@ function views_preprocess_page(&$variables) {
   // page.html.twig, so we can only find it using JavaScript. We therefore
   // remove the "contextual-region" class from the <body> tag here and add
   // JavaScript that will insert it back in the correct place.
-  if (!empty($variables['page']['#views_contextual_links']) && isset($variables['attributes']['class'])) {
-    /** @var \Drupal\Core\Page\HtmlPage $page_object */
-    $page_object = $variables['page']['#page'];
-    $attributes = $page_object->getBodyAttributes();
-    $class = $attributes['class'] ?: array();
-
-    $key = array_search('contextual-region', $variables['attributes']['class'] instanceof AttributeArray ? $variables['attributes']['class']->value() : $variables['attributes']['class']);
-    if ($key !== FALSE) {
-      /** @var \Drupal\Core\Page\HtmlPage $page_object */
-      unset($class[$key]);
-      $attributes['class'] = $class;
-      $attributes['data-views-page-contextual-id'] = $variables['title_suffix']['contextual_links']['#id'];
-      $variables['#attached']['library'][] = 'views/views.contextual-links';
-    }
+  if (!empty($variables['html']['page']['#views_contextual_links'])) {
+    $variables['attributes']['data-views-page-contextual-id'] = _contextual_links_to_id($variables['html']['page']['#contextual_links']);
   }
 }
 
@@ -460,6 +448,12 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable
               'display_id' => $display_id,
             ),
           );
+          // If we're setting contextual links on a page, for a page view, for a
+          // user that may use contextual links, attach Views' contextual links
+          // JavaScript.
+          if ($location === 'page' && $render_element['#type'] === 'page' && \Drupal::currentUser()->hasPermission('access contextual links')) {
+            $render_element['#attached']['library'][] = 'views/views.contextual-links';
+          }
         }
       }
     }
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseRendererTest.php
deleted file mode 100644
index 2c71d87..0000000
diff --git a/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php b/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php
new file mode 100644
index 0000000..de8f094
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Controller\AjaxControllerTest.
+ */
+
+namespace Drupal\Tests\Core\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Controller\AjaxController;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Controller\AjaxControllerTest
+ * @group Ajax
+ */
+class AjaxControllerTest extends UnitTestCase {
+
+  /**
+   * The tested ajax controller.
+   *
+   * @var \Drupal\Tests\Core\Controller\TestAjaxController
+   */
+  protected $ajaxController;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $controller_resolver->expects($this->any())
+      ->method('getArguments')
+      ->willReturn([]);
+    $element_info_manager = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
+    $element_info_manager->expects($this->any())
+      ->method('getInfo')
+      ->with('ajax')
+      ->willReturn([
+        '#header' => TRUE,
+        '#commands' => array(),
+        '#error' => NULL,
+      ]);
+    $this->ajaxController = new TestAjaxController($controller_resolver, $element_info_manager);
+  }
+
+  /**
+   * Tests the renderMainContent method.
+   *
+   * @covers \Drupal\Core\Controller\AjaxController::renderContentIntoResponse
+   */
+  public function testRenderWithFragmentObject() {
+    $main_content = ['#markup' => 'example content'];
+    /** @var \Drupal\Core\Ajax\AjaxResponse $result */
+    $result = $this->ajaxController->renderContentIntoResponse($main_content, '', []);
+
+    $this->assertInstanceOf('Drupal\Core\Ajax\AjaxResponse', $result);
+
+    $commands = $result->getCommands();
+    $this->assertEquals('insert', $commands[0]['command']);
+    $this->assertEquals('example content', $commands[0]['data']);
+
+    $this->assertEquals('insert', $commands[1]['command']);
+    $this->assertEquals('status_messages', $commands[1]['data']);
+  }
+
+  /**
+   * Tests the handle method with a Json response object.
+   *
+   * @covers \Drupal\Core\Controller\AjaxController::handle
+   */
+  public function testRenderWithResponseObject() {
+    $json_response = new JsonResponse(array('foo' => 'bar'));
+    $request = new Request();
+    $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $_content = function() use ($json_response) {
+      return $json_response;
+    };
+    $this->assertSame($json_response, $this->ajaxController->handle($request, $route_match, $_content));
+  }
+
+  /**
+   * Tests the handle method with an Ajax response object.
+   *
+   * @covers \Drupal\Core\Controller\AjaxController::handle
+   */
+  public function testRenderWithAjaxResponseObject() {
+    $ajax_response = new AjaxResponse(array('foo' => 'bar'));
+    $request = new Request();
+    $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $_content = function() use ($ajax_response) {
+      return $ajax_response;
+    };
+    $this->assertSame($ajax_response, $this->ajaxController->handle($request, $route_match, $_content));
+  }
+
+}
+
+class TestAjaxController extends AjaxController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
+    if (isset($elements['#markup'])) {
+      return $elements['#markup'];
+    }
+    elseif (isset($elements['#theme'])) {
+      return $elements['#theme'];
+    }
+    else {
+      return 'Markup';
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php b/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php
deleted file mode 100644
index f2305db..0000000
diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme
index 60f9a34..7588a8c 100644
--- a/core/themes/bartik/bartik.theme
+++ b/core/themes/bartik/bartik.theme
@@ -10,51 +10,49 @@
 use Drupal\Core\Template\Attribute;
 
 /**
- * Implements hook_preprocess_HOOK() for page.html.twig.
+ * Implements hook_preprocess_HOOK() for HTML document templates.
  *
  * Adds body classes if certain regions have content.
  */
-function bartik_preprocess_page(&$variables) {
+function bartik_preprocess_html(&$variables) {
   // Add information about the number of sidebars.
-  /** @var \Drupal\Core\Page\HtmlPage $page_object */
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getBodyAttributes();
-  $classes = $attributes['class'];
-  if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) {
-    $classes[] = 'layout-two-sidebars';
+  if (!empty($variables['html']['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) {
+    $variables['attributes']['class'][] = 'layout-two-sidebars';
   }
-  elseif (!empty($variables['page']['sidebar_first'])) {
-    $classes[] = 'layout-one-sidebar';
-    $classes[] = 'layout-sidebar-first';
+  elseif (!empty($variables['html']['page']['sidebar_first'])) {
+    $variables['attributes']['class'][] = 'layout-one-sidebar';
+    $variables['attributes']['class'][] = 'layout-sidebar-first';
   }
-  elseif (!empty($variables['page']['sidebar_second'])) {
-    $classes[] = 'layout-one-sidebar';
-    $classes[] = 'layout-sidebar-second';
+  elseif (!empty($variables['html']['page']['sidebar_second'])) {
+    $variables['attributes']['class'][] = 'layout-one-sidebar';
+    $variables['attributes']['class'][] = 'layout-sidebar-second';
   }
   else {
-    $classes[] = 'layout-no-sidebars';
+    $variables['attributes']['class'][] = 'layout-no-sidebars';
   }
 
-  if (!empty($variables['page']['featured'])) {
-    $classes[] = 'featured';
+  if (!empty($variables['html']['page']['featured'])) {
+    $variables['attributes']['class'][] = 'featured';
   }
 
-  if (!empty($variables['page']['triptych_first'])
-    || !empty($variables['page']['triptych_middle'])
-    || !empty($variables['page']['triptych_last'])) {
-    $classes[] = 'triptych';
+  if (!empty($variables['html']['page']['triptych_first'])
+    || !empty($variables['html']['page']['triptych_middle'])
+    || !empty($variables['html']['page']['triptych_last'])) {
+    $variables['attributes']['class'][] = 'triptych';
   }
 
-  if (!empty($variables['page']['footer_firstcolumn'])
-    || !empty($variables['page']['footer_secondcolumn'])
-    || !empty($variables['page']['footer_thirdcolumn'])
-    || !empty($variables['page']['footer_fourthcolumn'])) {
-    $classes[] = 'footer-columns';
+  if (!empty($variables['html']['page']['footer_firstcolumn'])
+    || !empty($variables['html']['page']['footer_secondcolumn'])
+    || !empty($variables['html']['page']['footer_thirdcolumn'])
+    || !empty($variables['html']['page']['footer_fourthcolumn'])) {
+    $variables['attributes']['class'][] = 'footer-columns';
   }
+}
 
-  // Store back the classes to the htmlpage object.
-  $attributes['class'] = $classes;
-
+/**
+ * Implements hook_preprocess_HOOK() for page templates.
+ */
+function bartik_preprocess_page(&$variables) {
   // Set the options that apply to both page and maintenance page.
   _bartik_process_page($variables);
 
diff --git a/core/themes/seven/css/theme/install-page.css b/core/themes/seven/css/theme/install-page.css
index c9d124d..395fb9b 100644
--- a/core/themes/seven/css/theme/install-page.css
+++ b/core/themes/seven/css/theme/install-page.css
@@ -5,7 +5,7 @@
  * Unfortunately we have to make our styling quite strong, to override the
  * .maintenance-page styling.
  */
-.install-background {
+.install-page {
   background-color: #1275b2;
   background-image:
     url(../../images/noise-low.png),
diff --git a/core/themes/seven/css/theme/maintenance-page.css b/core/themes/seven/css/theme/maintenance-page.css
index edf9018..f2f5a0e 100644
--- a/core/themes/seven/css/theme/maintenance-page.css
+++ b/core/themes/seven/css/theme/maintenance-page.css
@@ -2,7 +2,7 @@
  * @file
  * Maintenance theming.
  */
-.maintenance-background {
+body.maintenance-page {
   background-color: #e0e0d8;
   background-image: -webkit-radial-gradient(hsl(203, 2%, 90%), hsl(203, 2%, 95%));
   background-image: radial-gradient(hsl(203, 2%, 90%), hsl(203, 2%, 95%));
diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme
index 4863525..76ddfcc 100644
--- a/core/themes/seven/seven.theme
+++ b/core/themes/seven/seven.theme
@@ -10,24 +10,33 @@
 use Drupal\Core\Form\FormStateInterface;
 
 /**
- * Implements hook_preprocess_HOOK() for page templates.
+ * Implements hook_preprocess_HOOK() for HTML document templates.
  */
-function seven_preprocess_page(&$variables) {
-  /** @var \Drupal\Core\Page\HtmlPage $page_object */
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getBodyAttributes();
-  $classes = $attributes['class'];
+function seven_preprocess_html(&$variables) {
   // Add information about the number of sidebars.
-
   if (!empty($variables['page']['sidebar_first'])) {
-    $classes[] = 'layout-one-sidebar';
-    $classes[] = 'layout-sidebar-first';
+    $variables['attributes']['class'][] = 'one-sidebar';
+    $variables['attributes']['class'][] = 'sidebar-first';
   }
   else {
-    $classes[] = 'layout-no-sidebars';
+    $variables['attributes']['class'][] = 'no-sidebars';
   }
-  $attributes['class'] = $classes;
 
+  // If on a node add or edit page, add a node-layout class.
+  $path_args = explode('/', \Drupal::request()->getPathInfo());
+  if ($suggestions = theme_get_suggestions($path_args, 'page', '-')) {
+    foreach ($suggestions as $suggestion) {
+      if ($suggestion === 'page-node-edit' || strpos($suggestion, 'page-node-add') !== FALSE) {
+        $variables['attributes']['class'][] = drupal_html_class('node-form-layout');
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for page templates.
+ */
+function seven_preprocess_page(&$variables) {
   $variables['primary_local_tasks'] = $variables['tabs'];
   unset($variables['primary_local_tasks']['#secondary']);
   $variables['secondary_local_tasks'] = array(
@@ -145,12 +154,6 @@ function seven_element_info_alter(&$type) {
  * Implements hook_preprocess_install_page().
  */
 function seven_preprocess_install_page(&$variables) {
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getHtmlAttributes();
-  $classes = $attributes['class'];
-  $classes[] = 'install-background';
-  $attributes['class'] = $classes;
-
   // Seven has custom styling for the install page.
   $variables['#attached']['library'][] = 'seven/install-page';
 }
@@ -159,12 +162,6 @@ function seven_preprocess_install_page(&$variables) {
  * Implements hook_preprocess_maintenance_page().
  */
 function seven_preprocess_maintenance_page(&$variables) {
-  $page_object = $variables['page']['#page'];
-  $attributes = $page_object->getHtmlAttributes();
-  $classes = $attributes['class'];
-  $classes[] = 'maintenance-background';
-  $attributes['class'] = $classes;
-
   // Seven has custom styling for the maintenance page.
   $variables['#attached']['library'][] = 'seven/maintenance-page';
 }
@@ -208,15 +205,3 @@ function seven_form_node_form_alter(&$form, FormStateInterface $form_state) {
   $form['revision_information']['#type'] = 'container';
   $form['revision_information']['#group'] = 'meta';
 }
-
-function seven_preprocess_html(&$variables) {
-  // If on a node add or edit page, add a node-layout class.
-  $path_args = explode('/', \Drupal::request()->getPathInfo());
-  if ($suggestions = theme_get_suggestions($path_args, 'page', '-')) {
-    foreach ($suggestions as $suggestion) {
-      if ($suggestion === 'page-node-edit' || strpos($suggestion, 'page-node-add') !== FALSE) {
-        $variables['attributes']['class'][] = drupal_html_class('node-form-layout');
-      }
-    }
-  }
-}
