 core/authorize.php                                 |   3 +-
 core/core.services.yml                             |  56 ++--
 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                            | 104 +++----
 .../Drupal/Core/Controller/DialogController.php    | 157 -----------
 .../Drupal/Core/Controller/HtmlControllerBase.php  |  82 ------
 .../Drupal/Core/Controller/HtmlPageController.php  |  81 ------
 core/lib/Drupal/Core/CoreServiceProvider.php       |   3 +
 .../Core/Display/Annotation/DisplayVariant.php     |   5 +-
 .../ContentControllerSubscriber.php                |  37 ++-
 .../ContentFormControllerSubscriber.php            |   4 +-
 .../EventSubscriber/DefaultExceptionSubscriber.php |  73 +----
 .../Core/EventSubscriber/HtmlViewSubscriber.php    | 110 --------
 .../EventSubscriber/MainContentViewSubscriber.php  | 107 ++++++++
 .../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    |  80 ++++++
 .../Core/Render/BareHtmlPageRendererInterface.php  |  79 ++++++
 core/lib/Drupal/Core/Render/Element/Html.php       |   2 +-
 core/lib/Drupal/Core/Render/Element/Page.php       |   4 +-
 .../MainContent/AjaxRenderer.php}                  |  85 +-----
 .../Core/Render/MainContent/DialogRenderer.php     |  92 +++++++
 .../Core/Render/MainContent/HtmlRenderer.php       | 303 +++++++++++++++++++++
 .../MainContent/MainContentRendererInterface.php   |  38 +++
 .../MainContent/MainContentRenderersPass.php       |  33 +++
 .../Core/Render/MainContent/ModalRenderer.php      |  42 +++
 .../Render/PageDisplayVariantSelectionEvent.php    |  77 ++++++
 .../Plugin/DisplayVariant/SimplePageVariant.php    |  47 ++++
 core/lib/Drupal/Core/Render/RenderEvents.php       |  22 ++
 .../src/Authentication/Provider/BasicAuth.php      |   2 +-
 core/modules/block/block.module                    |  36 +--
 core/modules/block/block.services.yml              |   4 +
 .../block/src/Controller/BlockController.php       |  34 ++-
 .../BlockPageDisplayVariantSubscriber.php          |  40 +++
 .../{FullPageVariant.php => BlockPageVariant.php}  |  12 +-
 ...ageVariantTest.php => BlockPageVariantTest.php} |  10 +-
 .../modules/node/src/Tests/Views/FrontPageTest.php |   2 +-
 core/modules/rest/src/Tests/ReadTest.php           |   8 +-
 core/modules/rest/src/Tests/ResourceTest.php       |  12 +-
 core/modules/simpletest/simpletest.module          |   1 -
 .../system/src/Controller/BatchController.php      |  92 +------
 .../system/src/Controller/DbUpdateController.php   |  19 +-
 .../system/src/Tests/Common/AddFeedTest.php        |  18 +-
 .../system/src/Tests/Common/PageRenderTest.php     |   9 +-
 .../src/Tests/System/MainContentFallbackTest.php   |  21 +-
 core/modules/system/system.module                  |  80 ++++++
 core/modules/system/system.routing.yml             |   4 +-
 core/modules/system/templates/html.html.twig       |  10 +-
 .../ajax_test/src/Form/AjaxTestDialogForm.php      |   2 +-
 .../modules/ajax_test/src/Form/AjaxTestForm.php    |   2 +-
 .../src/EventSubscriber/HtmlPageSubscriber.php     |  45 ---
 .../system_module_test/system_module_test.module   |  13 +
 .../system_module_test.services.yml                |   5 -
 .../tests/modules/system_test/system_test.module   |  13 -
 core/modules/system/theme.api.php                  |  74 +++++
 core/modules/views/views.module                    |  24 +-
 ...AjaxControllerTest.php => AjaxRendererTest.php} |  58 +---
 core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php |  62 -----
 core/themes/bartik/bartik.libraries.yml            |   1 +
 core/themes/bartik/bartik.theme                    |  34 ++-
 core/themes/seven/css/theme/install-page.css       |   2 +-
 core/themes/seven/css/theme/maintenance-page.css   |   2 +-
 core/themes/seven/seven.theme                      |  55 ++--
 80 files changed, 1465 insertions(+), 2536 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 19967f6..f306a0d 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -650,15 +650,34 @@ 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:
-    class: Drupal\Core\Controller\AjaxController
-    arguments: ['@controller_resolver', '@element_info']
-  controller.dialog:
-    class: Drupal\Core\Controller\DialogController
-    arguments: ['@controller_resolver', '@title_resolver']
+
+  # Main content view subscriber plus the renderers it uses.
+  main_content_view_subscriber:
+    class: Drupal\Core\EventSubscriber\MainContentViewSubscriber
+    arguments: ['@class_resolver', '@current_route_match', '%main_content_renderers%']
+    tags:
+      - { name: event_subscriber }
+  main_content_renderer.html:
+    class: Drupal\Core\Render\MainContent\HtmlRenderer
+    arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler']
+    tags:
+      - { name: render.main_content_renderer, format: html }
+  main_content_renderer.ajax:
+    class: Drupal\Core\Render\MainContent\AjaxRenderer
+    arguments: ['@element_info']
+    tags:
+      - { name: render.main_content_renderer, format: drupal_ajax }
+  main_content_renderer.dialog:
+    class: Drupal\Core\Render\MainContent\DialogRenderer
+    arguments: ['@title_resolver']
+    tags:
+      - { name: render.main_content_renderer, format: drupal_dialog }
+  main_content_renderer.modal:
+    class: Drupal\Core\Render\MainContent\ModalRenderer
+    arguments: ['@title_resolver']
+    tags:
+      - { name: render.main_content_renderer, format: drupal_modal }
+
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
@@ -671,19 +690,8 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@content_negotiation', '@title_resolver']
-  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
+  bare_html_page_renderer:
+    class: Drupal\Core\Render\BareHtmlPageRenderer
   private_key:
     class: Drupal\Core\PrivateKey
     arguments: ['@state']
@@ -732,7 +740,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:
@@ -777,7 +785,7 @@ services:
     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 9e693f5..4f527b3 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 4163da2..001c9e7 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 d6e40d1..2adc576 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 e772289..493ceab 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -18,8 +18,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;
@@ -1646,22 +1644,37 @@ 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['page'] = $variables['html']['page'];
+  unset($variables['html']['page']);
+  $variables['page_top'] = NULL;
+  if (isset($variables['html']['page_top'])) {
+    $variables['page_top'] = $variables['html']['page_top'];
+    unset($variables['html']['page_top']);
+  }
+  $variables['page_bottom'] = NULL;
+  if (isset($variables['html']['page_bottom'])) {
+    $variables['page_bottom'] = $variables['html']['page_bottom'];
+    unset($variables['html']['page_bottom']);
+  }
 
-  $variables['html_attributes'] = $page->getHtmlAttributes();
-  $variables['attributes'] = $page->getBodyAttributes();
-  $variables['page'] = $page;
+  $variables['html_attributes'] = new Attribute();
+
+  // 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,
@@ -1669,20 +1682,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['page']['#title'])) {
     $head_title = array(
-      'title' => SafeMarkup::set(trim(strip_tags($page->getTitle()))),
+      'title' => SafeMarkup::set(trim(strip_tags($variables['page']['#title']))),
       'name' => String::checkPlain($site_config->get('name')),
     );
   }
@@ -1710,35 +1721,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['page']['#attached']);
+  if (isset($variables['page_top'])) {
+    $attached = drupal_merge_attached($attached, $variables['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['page_bottom'])) {
+    $attached = drupal_merge_attached($attached, $variables['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['styles'] = drupal_get_css();
+  $variables['scripts'] = drupal_get_js();
+  $variables['scripts_bottom'] = drupal_get_js('footer');
+  $variables['head'] = drupal_get_html_head(FALSE);
 }
 
 /**
@@ -1749,8 +1752,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();
@@ -1890,15 +1891,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';
@@ -1918,12 +1910,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());
@@ -2161,7 +2147,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/Controller/DialogController.php b/core/lib/Drupal/Core/Controller/DialogController.php
deleted file mode 100644
index 6aef7fa..0000000
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
deleted file mode 100644
index 88e0efe..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/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index db54f91..3767c9f 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\MainContent\MainContentRenderersPass;
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
 
 /**
@@ -54,6 +55,8 @@ public function register(ContainerBuilder $container) {
 
     $container->addCompilerPass(new StackedKernelPass());
 
+    $container->addCompilerPass(new MainContentRenderersPass());
+
     // 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 3c05cf3..3bf7492 100644
--- a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php
+++ b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php
@@ -25,8 +25,9 @@
  *
  * Plugin namespace: Plugin\DisplayVariant
  *
- * For a working example, see
- * \Drupal\block\Plugin\DisplayVariant\FullPageVariant
+ * For working examples, see
+ * - \Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant
+ * - \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
  *
  * @see \Drupal\Core\Display\VariantInterface
  * @see \Drupal\Core\Display\VariantBase
diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
index 377ae93..0d928705 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
@@ -13,7 +13,13 @@
 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.
+ *
+ * @todo Remove this event subscriber after both
+ *   https://www.drupal.org/node/2092647 and https://www.drupal.org/node/2331919
+ *   have landed.
+ *
+ * @see \Drupal\Core\EventSubscriber\MainContentViewSubscriber
  */
 class ContentControllerSubscriber implements EventSubscriberInterface {
 
@@ -35,20 +41,10 @@ public function __construct(ContentNegotiation $negotiation) {
   }
 
   /**
-   * 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.
    *
+   * @todo Remove when https://www.drupal.org/node/2331919 lands.
+   *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    *   The event to process.
    */
@@ -61,19 +57,20 @@ public function onRequestDeriveFormat(GetResponseEvent $event) {
   }
 
   /**
-   * Sets the _controller on a request based on the request format.
+   * Sets _content (if it exists) as the _controller.
+   *
+   * @todo Remove when https://www.drupal.org/node/2092647 lands.
    *
    * @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]);
-      }
+    $content = $request->attributes->get('_content');
+    if (empty($controller) && !empty($content)) {
+      $request->attributes->set('_controller',  $content);
     }
   }
 
@@ -85,7 +82,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/ContentFormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php
index 43bdd8b..b1d8411 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php
@@ -57,6 +57,8 @@ public function __construct(ClassResolverInterface $class_resolver, ControllerRe
   /**
    * Sets the _controller on a request based on the request format.
    *
+   * @todo Remove when https://www.drupal.org/node/2092647 lands.
+   *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    *   The event to process.
    */
@@ -65,7 +67,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) {
 
     if ($form = $request->attributes->get('_form')) {
       $wrapper = new HtmlFormController($this->classResolver, $this->controllerResolver, $this->container, $form, $this->formBuilder);
-      $request->attributes->set('_content', array($wrapper, 'getContentResult'));
+      $request->attributes->set('_controller', array($wrapper, 'getContentResult'));
     }
   }
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
index ad4dee7..88fac22 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
@@ -11,11 +11,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\ContentNegotiation;
-use Drupal\Core\Form\EnforcedResponse;
-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;
@@ -37,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.
@@ -65,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;
   }
 
   /**
@@ -147,7 +133,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) {
@@ -188,39 +174,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);
-
-    // Normally the EnforcedFormResponseSubscriber takes care of the
-    // EnforcedResponseException. But outside of HttpKernel::handleRaw(), it is
-    // necessary to catch and handle it manually.
-    try {
-      $page = $this->fragmentRenderer->render($fragment, $response_code);
-      return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode());
-    }
-    catch (\Exception $e) {
-      if ($response = EnforcedResponse::createFromException($e)) {
-        return $response;
-      }
-      else {
-        throw $e;
-      }
-    }
-  }
-
-  /**
    * 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/MainContentViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
new file mode 100644
index 0000000..fcc51a3
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\MainContentViewSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * View subscriber rendering main content render arrays into responses.
+ *
+ * Additional target rendering formats can be defined by adding another service
+ * that implements \Drupal\Core\Render\MainContent\MainContentRendererInterface
+ * and tagging it as a @code render.main_content_renderer @endcode, then
+ * \Drupal\Core\Render\MainContent\MainContentRenderersPass will detect it and
+ * use it when appropriate.
+ *
+ * @see \Drupal\Core\Render\MainContent\MainContentRendererInterface
+ * @see \Drupal\Core\Render\MainContentControllerPass
+ */
+class MainContentViewSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The class resolver service.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $classResolver;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The available main content renderer services, keyed per format.
+   *
+   * @var array
+   */
+  protected $mainContentRenderers;
+
+  /**
+   * Constructs a new MainContentViewSubscriber object.
+   *
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   The class resolver service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param array $main_content_renderers
+   *   The available main content renderer service IDs, keyed by format.
+   */
+  public function __construct(ClassResolverInterface $class_resolver, RouteMatchInterface $route_match, array $main_content_renderers) {
+    $this->classResolver = $class_resolver;
+    $this->routeMatch = $route_match;
+    $this->mainContentRenderers = $main_content_renderers;
+  }
+
+  /**
+   * Sets a response given a (main content) render array.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
+   *   The event to process.
+   */
+  public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
+    $request = $event->getRequest();
+    $result = $event->getControllerResult();
+
+    $format = $request->getRequestFormat();
+
+    // Render the controller result into a response if it's a render array.
+    if (is_array($result)) {
+      if (isset($this->mainContentRenderers[$format])) {
+        $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$format]);
+        $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
+      }
+      else {
+        $supported_formats = array_keys($this->mainContentRenderers);
+        $supported_mimetypes = array_map([$request, 'getMimeType'], $supported_formats);
+        $event->setResponse(new JsonResponse([
+          'message' => 'Not Acceptable.',
+          'supported_mime_types' => $supported_mimetypes,
+        ], 406));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::VIEW][] = ['onViewRenderArray'];
+
+    return $events;
+  }
+
+}
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..a8904e6
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -0,0 +1,80 @@
+<?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\Render\MainContent\HtmlRenderer::renderResponse() for more
+    // information about this; the exact same pattern is used there and
+    // explained in detail there.
+    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..7450199
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRendererInterface.php
@@ -0,0 +1,79 @@
+<?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, this will cause a main content renderer
+ * (\Drupal\Core\Render\MainContent\MainContentRendererInterface) to be used,
+ * and in case of a HTML request that will be
+ * \Drupal\Core\Render\MainContent\HtmlRenderer.
+ *
+ * In fact, this is not only *typically* used in a limited environment, it even
+ * *must* be used in a limited environment: when using the bare HTML page
+ * renderer, use as little state/additional services as possible, because the
+ * same safeguards aren't present (precisely because this is intended to be used
+ * in a limited environment).
+ *
+ * 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\Render\MainContent\HtmlRenderer
+ */
+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..aa4a7d4 100644
--- a/core/lib/Drupal/Core/Render/Element/Html.php
+++ b/core/lib/Drupal/Core/Render/Element/Html.php
@@ -8,7 +8,7 @@
 namespace Drupal\Core\Render\Element;
 
 /**
- * Provides a render element for <html>.
+ * Provides a render element for an entire HTML page: <html> plus its children.
  *
  * @RenderElement("html")
  */
diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php
index cf133af..0d8b777 100644
--- a/core/lib/Drupal/Core/Render/Element/Page.php
+++ b/core/lib/Drupal/Core/Render/Element/Page.php
@@ -8,7 +8,9 @@
 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.
  *
  * @RenderElement("page")
  */
diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
similarity index 42%
rename from core/lib/Drupal/Core/Controller/AjaxController.php
rename to core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
index 79ee2c0..766dc2d 100644
--- a/core/lib/Drupal/Core/Controller/AjaxController.php
+++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
@@ -2,28 +2,23 @@
 
 /**
  * @file
- * Contains \Drupal\Core\Controller\AjaxController.
+ * Contains \Drupal\Core\Render\MainContent\AjaxRenderer.
  */
 
-namespace Drupal\Core\Controller;
+namespace Drupal\Core\Render\MainContent;
 
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\AlertCommand;
 use Drupal\Core\Ajax\InsertCommand;
 use Drupal\Core\Ajax\PrependCommand;
-use Drupal\Core\Page\HtmlFragment;
 use Drupal\Core\Render\ElementInfoManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
 
 /**
- * Default controller for Ajax requests.
+ * Default main content renderer for Ajax requests.
  */
-class AjaxController implements ContainerAwareInterface {
-
-  use ContainerAwareTrait;
+class AjaxRenderer implements MainContentRendererInterface {
 
   /**
    * The controller resolver.
@@ -40,55 +35,26 @@ class AjaxController implements ContainerAwareInterface {
   protected $elementInfoManager;
 
   /**
-   * Constructs a new AjaxController instance.
+   * Constructs a new AjaxRenderer instance.
    *
-   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
-   *   The controller resolver.
    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager
    *   The element info manager.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver, ElementInfoManagerInterface $element_info_manager) {
-    $this->controllerResolver = $controller_resolver;
+  public function __construct(ElementInfoManagerInterface $element_info_manager) {
     $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);
-
-    // If there is already a Response object, return it without manipulation.
-    if ($content instanceof Response && $content->isOk()) {
-      return $content;
-    }
-
-    // Allow controllers to return an HtmlFragment directly.
-    if ($content instanceof HtmlFragment) {
-      $content = $content->getContent();
-    }
-    // Most controllers return a render array, but some return a string.
-    if (!is_array($content)) {
-      $content = array(
-        '#markup' => $content,
-      );
-    }
-
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
     $response = new AjaxResponse();
 
-    if (isset($content['#type']) && ($content['#type'] == 'ajax')) {
+    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.
-      $content += $this->elementInfoManager->getInfo('ajax');
-      $error = $content['#error'];
+      $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)) {
@@ -98,7 +64,7 @@ public function content(Request $request, $_content) {
       }
     }
 
-    $html = $this->drupalRenderRoot($content);
+    $html = $this->drupalRenderRoot($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'
@@ -113,31 +79,6 @@ public function content(Request $request, $_content) {
   }
 
   /**
-   * 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.
-   */
-  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;
-  }
-
-  /**
    * Wraps drupal_render_root().
    *
    * @todo: Remove as part of https://drupal.org/node/2182149
diff --git a/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
new file mode 100644
index 0000000..9f59944
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\DialogRenderer.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\OpenDialogCommand;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default main content renderer for dialog requests.
+ */
+class DialogRenderer implements MainContentRendererInterface {
+
+  /**
+   * The title resolver.
+   *
+   * @var \Drupal\Core\Controller\TitleResolverInterface
+   */
+  protected $titleResolver;
+
+  /**
+   * Constructs a new DialogRenderer.
+   *
+   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
+   *   The title resolver.
+   */
+  public function __construct(TitleResolverInterface $title_resolver) {
+    $this->titleResolver = $title_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    $response = new AjaxResponse();
+
+    // First render the main content, because it might provide a title.
+    $content = drupal_render_root($main_content);
+    drupal_process_attached($main_content);
+
+    // Determine the title: use the title provided by the main content if any,
+    // otherwise get it from the routing information.
+    $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
+
+    // Determine the dialog options and the target for the OpenDialogCommand.
+    $options = $request->request->get('dialogOptions', array());
+    $target = $this->determineTargetSelector($options, $route_match);
+
+    $response->addCommand(new OpenDialogCommand($target, $title, $content, $options));
+    return $response;
+  }
+
+  /**
+   * Determine the target selector for the OpenDialogCommand.
+   *
+   * @param array &$options
+   *   The 'target' option, if set, is used, and then removed from $options.
+   * @param RouteMatchInterface $route_match
+   *   When no 'target' option is set in $options, $route_match is used instead
+   *   to determine the target.
+   *
+   * @return string
+   *   The target selector.
+   */
+  protected function determineTargetSelector(array &$options, RouteMatchInterface $route_match) {
+    // 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");
+    }
+    return $target;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
new file mode 100644
index 0000000..739263a
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\HtmlRenderer.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Display\PageVariantInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
+use Drupal\Core\Render\RenderEvents;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Default main content renderer for HTML requests.
+ */
+class HtmlRenderer implements MainContentRendererInterface {
+
+  /**
+   * 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;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a new HtmlRenderer.
+   *
+   * @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.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler) {
+    $this->titleResolver = $title_resolver;
+    $this->displayVariantManager = $display_variant_manager;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    list($page, $title) = $this->prepare($main_content, $request, $route_match);
+
+    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.
+    $this->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($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)'
+    ]);
+  }
+
+  /**
+   * Prepares the HTML body: wraps the main content in #type 'page'.
+   *
+   * @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 two values:
+   *   0. A #type 'page' render array.
+   *   1. The page title.
+   *
+   * @throws \LogicException
+   *   If the selected display variant does not implement PageVariantInterface.
+   */
+  protected function prepare(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', $route_match);
+      $this->eventDispatcher->dispatch(RenderEvents::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' => $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 title provided by the main content if any,
+    // otherwise get it from the routing information.
+    $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
+
+    return [$page, $title];
+  }
+
+  /**
+   * 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
+   *
+   * @internal
+   *
+   * @see hook_page_attachments()
+   * @see hook_page_attachments_alter()
+   */
+  public function invokePageAttachmentHooks(array &$page) {
+    // Modules can add attachments.
+    $attachments = [];
+    foreach ($this->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.
+    $this->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().');
+    }
+
+    // Merge the attachments onto the $page render array.
+    $page['#attached'] = isset($page['#attached']) ? $page['#attached'] : [];
+    $page['#post_render_cache'] = isset($page['#post_render_cache']) ? $page['#post_render_cache'] : [];
+    if (isset($attachments['#attached'])) {
+      $page['#attached'] = drupal_merge_attached($page['#attached'], $attachments['#attached']);
+    }
+    if (isset($attachments['#post_render_cache'])) {
+      $page['#post_render_cache'] = NestedArray::mergeDeep($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
+   *
+   * @internal
+   *
+   * @see hook_page_top()
+   * @see hook_page_bottom()
+   * @see html.html.twig
+   */
+  public function buildPageTopAndBottom(array &$html) {
+    // Modules can add render arrays to the top and bottom of the page.
+    $page_top = [];
+    $page_bottom = [];
+    foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
+      $function = $module . '_page_top';
+      $function($page_top);
+    }
+    foreach ($this->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/Render/MainContent/MainContentRendererInterface.php b/core/lib/Drupal/Core/Render/MainContent/MainContentRendererInterface.php
new file mode 100644
index 0000000..ca957c8
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/MainContentRendererInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\MainContentRendererInterface.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * The interface for "main content" (@code _content @endcode) renderers.
+ *
+ * Classes implementing this interface are able to render the main content (as
+ * received from "_content" controllers) into a response of  a certain format
+ * (HTML, JSON …) and/or in a certain decorated manner (e.g. in the case of the
+ * default HTML main content renderer: with a page display variant applied).
+ */
+interface MainContentRendererInterface  {
+
+  /**
+   * Renders the main content render array into a response.
+   *
+   * @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 \Symfony\Component\HttpFoundation\Response
+   *   The Response in the format that this implementation supports.
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match);
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/MainContentRenderersPass.php b/core/lib/Drupal/Core/Render/MainContent/MainContentRenderersPass.php
new file mode 100644
index 0000000..08b4665
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/MainContentRenderersPass.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\MainContentRenderersPass.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+/**
+ * Adds main_content_renderers parameter to the container.
+ */
+class MainContentRenderersPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Collects the available main content renderer service IDs into the
+   * main_content_renderers parameter, keyed by format.
+   */
+  public function process(ContainerBuilder $container) {
+    $main_content_renderers = [];
+    foreach ($container->findTaggedServiceIds('render.main_content_renderer') as $id => $attributes) {
+      $format = $attributes[0]['format'];
+      $main_content_renderers[$format] = $id;
+    }
+    $container->setParameter('main_content_renderers', $main_content_renderers);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php b/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php
new file mode 100644
index 0000000..c971eb3
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\MainContent\ModalRenderer.
+ */
+
+namespace Drupal\Core\Render\MainContent;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Render\MainContent\DialogRenderer;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default main content renderer for modal dialog requests.
+ */
+class ModalRenderer extends DialogRenderer {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    $response = new AjaxResponse();
+
+    // First render the main content, because it might provide a title.
+    $content = drupal_render_root($main_content);
+    drupal_process_attached($main_content);
+
+    // If the main content doesn't provide a title, use the title resolver.
+    $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
+
+    // Determine the title: use the title provided by the main content if any,
+    // otherwise get it from the routing information.
+    $options = $request->request->get('dialogOptions', array());
+
+    $response->addCommand(new OpenModalDialogCommand($title, $content, $options));
+    return $response;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/PageDisplayVariantSelectionEvent.php b/core/lib/Drupal/Core/Render/PageDisplayVariantSelectionEvent.php
new file mode 100644
index 0000000..17771bd
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/PageDisplayVariantSelectionEvent.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\PageDisplayVariantSelectionEvent.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event fired when rendering main content, to select a page display variant.
+ *
+ * @see \Drupal\Core\Render\RenderEvents::SELECT_PAGE_DISPLAY_VARIANT
+ * @see \Drupal\Core\Render\MainContent\HtmlRenderer
+ */
+class PageDisplayVariantSelectionEvent extends Event {
+
+  /**
+   * The selected page display variant plugin ID.
+   *
+   * @var string
+   */
+  protected $pluginId;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs the page display variant plugin selection event.
+   *
+   * @param string
+   *   The ID of the page display variant plugin to use by default.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match, for context.
+   */
+  public function __construct($plugin_id, RouteMatchInterface $route_match) {
+    $this->pluginId = $plugin_id;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Gets the current route match.
+   *
+   * @return \Drupal\Core\Routing\RouteMatchInterface
+   *   The current route match, for context.
+   */
+  public function getRouteMatch() {
+    return $this->routeMatch;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Plugin/DisplayVariant/SimplePageVariant.php b/core/lib/Drupal/Core/Render/Plugin/DisplayVariant/SimplePageVariant.php
new file mode 100644
index 0000000..7772875
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Plugin/DisplayVariant/SimplePageVariant.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant.
+ */
+
+namespace Drupal\Core\Render\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/lib/Drupal/Core/Render/RenderEvents.php b/core/lib/Drupal/Core/Render/RenderEvents.php
new file mode 100644
index 0000000..8b0fa55
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/RenderEvents.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\RenderEvents.
+ */
+
+namespace Drupal\Core\Render;
+
+/**
+ * Defines events for the render system.
+ */
+final class RenderEvents {
+
+  /**
+   * Name of the event when selecting a page display variant to use.
+   *
+   * @see \Drupal\Core\Render\PageDisplayVariantSelectionEvent
+   */
+  const SELECT_PAGE_DISPLAY_VARIANT = 'render.page_display_variant.select';
+
+}
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 83f4a40..a294dc8 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -62,44 +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')
-      ->setMainContent(drupal_set_page_content())
-      ->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 0b6b4ae..6442712 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -3,6 +3,10 @@ services:
     class: Drupal\block\Theme\AdminDemoNegotiator
     tags:
       - { name: theme_negotiator, priority: 1000 }
+  block.page_display_variant_subscriber:
+    class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
+    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/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php
index a4e7952..6207122 100644
--- a/core/modules/block/src/Controller/BlockController.php
+++ b/core/modules/block/src/Controller/BlockController.php
@@ -52,11 +52,12 @@ public static function create(ContainerInterface $container) {
    *   The name of the theme.
    *
    * @return array
-   *   A render array containing the CSS and title for the block region demo.
+   *   A #type 'page' render array containing the block region demo.
    */
   public function demo($theme) {
-    return array(
-      '#title' => String::checkPlain($this->themeHandler->getName($theme)),
+    $page = [
+      '#title' => $this->themeHandler->getName($theme),
+      '#type' => 'page',
       '#attached' => array(
         'js' => array(
           array(
@@ -72,7 +73,32 @@ public function demo($theme) {
           'block/drupal.block.admin',
         ),
       ),
-    );
+    ];
+
+    // Show descriptions in each visible page region, nothing else.
+    $visible_regions = $this->getVisibleRegionNames($theme);
+    foreach (array_keys($visible_regions) as $region) {
+      $page[$region]['block_description'] = array(
+        '#type' => 'inline_template',
+        '#template' => '<div class="block-region demo-block">{{ region_name }}</div>',
+        '#context' => array('region_name' => $visible_regions[$region]),
+      );
+    }
+
+    return $page;
+  }
+
+  /**
+   * Returns the human-readable list of regions keyed by machine name.
+   *
+   * @param string $theme
+   *   The name of the theme.
+   *
+   * @return array
+   *   An array of human-readable region names keyed by machine name.
+   */
+  protected function getVisibleRegionNames($theme) {
+    return system_region_list($theme, REGIONS_VISIBLE);
   }
 
 }
diff --git a/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php
new file mode 100644
index 0000000..f66b968
--- /dev/null
+++ b/core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber.
+ */
+
+namespace Drupal\block\EventSubscriber;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
+use Drupal\Core\Render\RenderEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Selects the block page display variant.
+ *
+ * @see \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
+ */
+class BlockPageDisplayVariantSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Selects the block page display variant.
+   *
+   * @param \Drupal\Core\Render\PageDisplayVariantSelectionEvent $event
+   *   The event to process.
+   */
+  public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) {
+    $event->setPluginId('block_page');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[RenderEvents::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 90%
rename from core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php
rename to core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
index 8bc0f3a..fe78cb9 100644
--- a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php
+++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\block\Plugin\DisplayVariant\FullPageVariant.
+ * Contains \Drupal\block\Plugin\DisplayVariant\BlockPageVariant.
  */
 
 namespace Drupal\block\Plugin\DisplayVariant;
@@ -16,14 +16,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.
  *
  * @PageDisplayVariant(
- *   id = "full_page",
- *   admin_label = @Translation("Full page")
+ *   id = "block_page",
+ *   admin_label = @Translation("Page with blocks")
  * )
  */
-class FullPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface {
+class BlockPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface {
 
   /**
    * The block repository.
@@ -54,7 +54,7 @@ class FullPageVariant extends VariantBase implements PageVariantInterface, Conta
   protected $mainContent = [];
 
   /**
-   * Constructs a new FullPageVariant.
+   * Constructs a new BlockPageVariant.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.
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 93%
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 7efe1c1..e16d133 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 repository.
@@ -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,7 +59,7 @@ 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->blockRepository, $this->blockViewBuilder, $this->routeMatch, $this->themeNegotiator))
       ->setMethods(array('getRegionNames'))
       ->getMock();
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/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php
index e1aae7f..afa178b 100644
--- a/core/modules/rest/src/Tests/ReadTest.php
+++ b/core/modules/rest/src/Tests/ReadTest.php
@@ -87,8 +87,12 @@ public function testRead() {
     $account = $this->drupalCreateUser();
     $this->drupalLogin($account);
     $response = $this->httpRequest($account->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
-    $this->assertResponse(404);
-    $expected_message = Json::encode(['error' => 'A fatal error occurred: Unable to find the controller for path "/user/4". Maybe you forgot to add the matching route in your routing configuration?']);
+    // AcceptHeaderMatcher considers the canonical, non-REST route a match, but
+    // a lower quality one: no format restrictions means there's always a match,
+    // and hence when there is no matching REST route, the non-REST route is
+    // used, but it can't render into application/hal+json, so it returns a 406.
+    $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
+    $expected_message = '{"message":"Not Acceptable.","supported_mime_types":["text\\/html","application\\/vnd.drupal-ajax","application\\/vnd.drupal-dialog","application\\/vnd.drupal-modal"]}';
     $this->assertIdentical($expected_message, $response);
   }
 
diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php
index 729af5c..35e1807 100644
--- a/core/modules/rest/src/Tests/ResourceTest.php
+++ b/core/modules/rest/src/Tests/ResourceTest.php
@@ -56,7 +56,11 @@ public function testFormats() {
 
     // Verify that accessing the resource returns 401.
     $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
-    $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define formats.');
+    // AcceptHeaderMatcher considers the canonical, non-REST route a match, but
+    // a lower quality one: no format restrictions means there's always a match,
+    // and hence when there is no matching REST route, the non-REST route is
+    // used, but it can't render into application/hal+json, so it returns a 406.
+    $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
     $this->curlClose();
   }
 
@@ -81,7 +85,11 @@ public function testAuthentication() {
 
     // Verify that accessing the resource returns 401.
     $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
-    $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define authentication.');
+    // AcceptHeaderMatcher considers the canonical, non-REST route a match, but
+    // a lower quality one: no format restrictions means there's always a match,
+    // and hence when there is no matching REST route, the non-REST route is
+    // used, but it can't render into application/hal+json, so it returns a 406.
+    $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
     $this->curlClose();
   }
 
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/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..e660448 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\Render\MainContent\HtmlRenderer;
 use Drupal\simpletest\KernelTestBase;
 
 /**
@@ -59,16 +60,18 @@ function testHookPageAttachmentsAlter() {
    *   The page render hook to assert expected exceptions for.
    */
   function assertPageRenderHookExceptions($module, $hook) {
+    $html_renderer = \Drupal::getContainer()->get('main_content_renderer.html');
+
     // Assert a valid hook implementation doesn't trigger an exception.
     $page = [];
-    drupal_prepare_page($page);
+    $html_renderer->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);
+      $html_renderer->invokePageAttachmentHooks($page);
       $this->error($assertion);
     }
     catch (\LogicException $e) {
@@ -82,7 +85,7 @@ function assertPageRenderHookExceptions($module, $hook) {
     $assertion = $hook . '() implementation that sets a child render array triggers an exception';
     $page = [];
     try {
-      drupal_prepare_page($page);
+      $html_renderer->invokePageAttachmentHooks($page);
       $this->error($assertion);
     }
     catch (\LogicException $e) {
diff --git a/core/modules/system/src/Tests/System/MainContentFallbackTest.php b/core/modules/system/src/Tests/System/MainContentFallbackTest.php
index 288e681..ae10847 100644
--- a/core/modules/system/src/Tests/System/MainContentFallbackTest.php
+++ b/core/modules/system/src/Tests/System/MainContentFallbackTest.php
@@ -10,7 +10,7 @@
 use Drupal\simpletest\WebTestBase;
 
 /**
- *  Test system module main content rendering fallback.
+ *  Test SimplePageVariant main content rendering fallback page display variant.
  *
  * @group system
  */
@@ -42,7 +42,7 @@ protected function setUp() {
   }
 
   /**
-   * Test availability of main content.
+   * Test availability of main content: Drupal falls back to SimplePageVariant.
    */
   function testMainContentFallback() {
     $edit = array();
@@ -54,20 +54,13 @@ function testMainContentFallback() {
     $this->rebuildContainer();
     $this->assertFalse(\Drupal::moduleHandler()->moduleExists('block'), 'Block module uninstall.');
 
-    // At this point, no region is filled and fallback should be triggered.
+    // When Block module is not installed and BlockPageVariant is not available,
+    // Drupal should fall back to SimplePageVariant. Both for the admin and the
+    // front-end theme.
     $this->drupalGet('admin/config/system/site-information');
-    $this->assertField('site_name', 'Admin interface still available.');
-
-    // Fallback should not trigger when another module is handling content.
-    $this->drupalGet('system-test/main-content-handling');
-    $this->assertRaw('id="system-test-content"', 'Content handled by another module');
-    $this->assertNoText(t('Content to test main content fallback'), 'Main content not displayed.');
-
-    // Fallback should trigger when another module
-    // indicates that it is not handling the content.
+    $this->assertField('site_name', 'Fallback to SimplePageVariant works for admin theme.');
     $this->drupalGet('system-test/main-content-fallback');
-    $this->assertText(t('Content to test main content fallback'), 'Main content fallback properly triggers.');
-
+    $this->assertText(t('Content to test main content fallback'), 'Fallback to SimplePageVariant works for front-end theme.');
     // Request a user* page and see if it is displayed.
     $this->drupalLogin($this->web_user);
     $this->drupalGet('user/' . $this->web_user->id() . '/edit');
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 0d41bba..33859e8 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -5,6 +5,7 @@
  * Configuration system that lets administrators modify the workings of the site.
  */
 
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionDiscovery;
@@ -531,6 +532,85 @@ function system_page_attachments(array &$page) {
     $page['#attached']['library'][] = 'system/admin';
   }
 
+  // Attach libraries and CSS used by this theme.
+  $active_theme = \Drupal::theme()->getActiveTheme();
+  foreach ($active_theme->getLibraries() as $library) {
+    $page['#attached']['library'][] = $library;
+  }
+  foreach ($active_theme->getStyleSheets() as $media => $stylesheets) {
+    foreach ($stylesheets as $stylesheet) {
+      $page['#attached']['css'][$stylesheet] = array(
+        'group' => CSS_AGGREGATE_THEME,
+        'every_page' => TRUE,
+        'media' => $media
+      );
+    }
+  }
+
+  // Attach favicon.
+  if (theme_get_setting('features.favicon')) {
+    $favicon = theme_get_setting('favicon.url');
+    $type = theme_get_setting('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) {
+    $page['#attached']['html_head'][] = [$value, $key];
+  }
+
   // Handle setting the "active" class on links by:
   // - loading the active-link library if the current user is authenticated;
   // - applying a post-render cache callback if the current user is anonymous.
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 4d54d00..26317f6 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -419,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..5ef96cd 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 }}
+    {{ head }}
     <title>{{ head_title }}</title>
-    {{ page.styles }}
-    {{ page.scripts }}
+    {{ styles }}
+    {{ 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 }}
     {{ page_bottom }}
-    {{ page.scripts('footer') }}
+    {{ scripts_bottom }}
   </body>
 </html>
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
index 21b3ac6..946504d 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
@@ -16,7 +16,7 @@
 use Drupal\Core\Form\FormStateInterface;
 
 /**
- * Dummy form for testing DialogController with _form routes.
+ * Dummy form for testing DialogRenderer with _form routes.
  */
 class AjaxTestDialogForm extends FormBase {
 
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
index 4155b32..677532f 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
@@ -11,7 +11,7 @@
 use Drupal\Core\Form\FormStateInterface;
 
 /**
- * Dummy form for testing DialogController with _form routes.
+ * Dummy form for testing DialogRenderer with _form routes.
  */
 class AjaxTestForm implements FormInterface {
 
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..69c9294 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,16 @@
  * Provides System module hook implementations for testing purposes.
  */
 
+/**
+ * Implements hook_page_attachments_alter().
+ */
+function system_module_test_page_attachments_alter(&$page) {
+  // Remove the HTML5 mobile meta-tags.
+  $meta_tags_to_remove = ['MobileOptimized', 'HandheldFriendly', 'viewport', 'cleartype'];
+  foreach ($page['#attached']['html_head'] as $index => $parts) {
+    if (in_array($parts[1], $meta_tags_to_remove)) {
+      unset($page['#attached']['html_head'][$index]);
+    }
+  }
+
+}
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/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index 8024680..b44e0a0 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -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/theme.api.php b/core/modules/system/theme.api.php
index f434122..1ba2efc 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -302,6 +302,80 @@
  *
  * 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.
+ *
+ * Any route that returns the "main content" as a render array automatically has
+ * the ability to be requested 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).
+ *
+ * After the controller returned a render array, the @code VIEW @endcode event
+ * (\Symfony\Component\HttpKernel\KernelEvents::VIEW) will be triggered, because
+ * the controller result is not a Response, but a render array.
+ *
+ * \Drupal\Core\EventSubscriber\MainContentViewSubscriber is subscribed to the
+ * @code VIEW @endcode event. It checks whether the controller result is an
+ * array, and if so, guarantees to generate a Response.
+ *
+ * Next, it checks whether the negotiated request format is supported. Any
+ * format for which a main content renderer service exists (an implementation of
+ * \Drupal\Core\Render\MainContent\MainContentRendererInterface) is supported.
+ *
+ * If the negotiated request format is not supported, a 406 JSON response is
+ * generated, which lists the supported formats in a machine-readable way(as per
+ * RFC 2616, section 10.4.7).
+ *
+ * Otherwise, when the negotiated request format is supported, the corresponding
+ * main content renderer service is initialized. A response is generated by
+ * calling \Drupal\Core\Render\MainContent\MainContentRendererInterface::renderResponse()
+ * on the service. That's it!
+ *
+ * Each main content renderer service can choose how to implement its
+ * renderResponse() method. It may of course choose to add protected helper
+ * methods to provide more structure, if it's a complex main content renderer.
+ *
+ * The above is the general flow. But let's take a look at the HTML main content
+ * renderer (\Drupal\Core\Render\MainContent\HtmlRenderer), because that will be
+ * used most often.
+ *
+ * \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() first calls a
+ * helper method, @code prepare() @endcode, which takes the main content render
+ * array and returns a #type 'page' render array. A #type 'page' render array
+ * represents the final <body> for the HTML document (page.html.twig). The
+ * remaining task for @code renderResponse() @endcode is to wrap the #type
+ * 'page' render array in a #type 'html' render array, which then represents the
+ * entire HTML document (html.html.twig).
+ * Hence the steps are:
+ * 1. \Drupal\Core\Render\MainContent\HtmlRenderer::prepare() takes the main
+ *    content render array; if it already is #type 'page', then most of the work
+ *    it must do is already done. In the other case, we need to build that #type
+ *    'page' render array still. The RenderEvents::SELECT_PAGE_DISPLAY_VARIANT
+ *    event is dispatched, to select a page display variant. By default,
+ *    \Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant is used, which
+ *    doesn't apply any decorations. But, when Block module is enabled,
+ *    \Drupal\block\Plugin\DisplayVariant\BlockPageVariant is used, which allows
+ *    the site builder to place blocks in any of the page regions, and hence
+ *    "decorate" the main content.
+ * 2. \Drupal\Core\Render\MainContent\HtmlRenderer::prepare() now is guaranteed
+ *    to be working on a #type 'page' render array. hook_page_attachments() and
+ *    hook_page_attachments_alter() are invoked.
+ * 3. \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() uses the
+ *    #type 'page' render array returned by the previous step and wraps it in
+ *    #type 'html'. hook_page_top() and hook_page_bottom() are invoked.
+ * 4. drupal_render() is called on the #type 'html' render array, which uses
+ *    the html.html.twig template and the return value is a HTML document as a
+ *    string.
+ * 5. This string of HTML is returned as the Response.
+ *
+ * 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 c216872..1c5da7c 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['page']['#views_contextual_links'])) {
+    $variables['attributes']['data-views-page-contextual-id'] = _contextual_links_to_id($variables['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/Controller/AjaxControllerTest.php b/core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php
similarity index 46%
rename from core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php
rename to core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php
index 688b8cf..bb5b823 100644
--- a/core/tests/Drupal/Tests/Core/Controller/AjaxControllerTest.php
+++ b/core/tests/Drupal/Tests/Core/Controller/AjaxRendererTest.php
@@ -2,38 +2,32 @@
 
 /**
  * @file
- * Contains \Drupal\Tests\Core\Controller\AjaxControllerTest.
+ * Contains \Drupal\Tests\Core\Controller\AjaxRendererTest.
  */
 
 namespace Drupal\Tests\Core\Controller;
 
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Controller\AjaxController;
+use Drupal\Core\Render\MainContent\AjaxRenderer;
 use Drupal\Tests\UnitTestCase;
-use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
- * @coversDefaultClass \Drupal\Core\Controller\AjaxController
+ * @coversDefaultClass \Drupal\Core\Render\MainContent\AjaxRenderer
  * @group Ajax
  */
-class AjaxControllerTest extends UnitTestCase {
+class AjaxRendererTest extends UnitTestCase {
 
   /**
    * The tested ajax controller.
    *
-   * @var \Drupal\Tests\Core\Controller\TestAjaxController
+   * @var \Drupal\Core\Render\MainContent\AjaxRenderer
    */
-  protected $ajaxController;
+  protected $ajaxRenderer;
 
   /**
    * {@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')
@@ -43,22 +37,20 @@ protected function setUp() {
         '#commands' => array(),
         '#error' => NULL,
       ]);
-    $this->ajaxController = new TestAjaxController($controller_resolver, $element_info_manager);
+    $this->ajaxRenderer = new TestAjaxRenderer($element_info_manager);
   }
 
   /**
    * Tests the content method.
    *
-   * @covers ::content
+   * @covers \Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse
    */
   public function testRenderWithFragmentObject() {
     $main_content = ['#markup' => 'example content'];
     $request = new Request();
-    $_content = function() use ($main_content) {
-      return $main_content;
-    };
+    $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
     /** @var \Drupal\Core\Ajax\AjaxResponse $result */
-    $result = $this->ajaxController->content($request, $_content);
+    $result = $this->ajaxRenderer->renderResponse($main_content, $request, $route_match);
 
     $this->assertInstanceOf('Drupal\Core\Ajax\AjaxResponse', $result);
 
@@ -70,37 +62,9 @@ public function testRenderWithFragmentObject() {
     $this->assertEquals('status_messages', $commands[1]['data']);
   }
 
-  /**
-   * Tests the content method with a Json response object.
-   *
-   * @covers ::content
-   */
-  public function testRenderWithResponseObject() {
-    $json_response = new JsonResponse(array('foo' => 'bar'));
-    $request = new Request();
-    $_content = function() use ($json_response) {
-      return $json_response;
-    };
-    $this->assertSame($json_response, $this->ajaxController->content($request, $_content));
-  }
-
-  /**
-   * Tests the content method with an Ajax response object.
-   *
-   * @covers ::content
-   */
-  public function testRenderWithAjaxResponseObject() {
-    $ajax_response = new AjaxResponse(array('foo' => 'bar'));
-    $request = new Request();
-    $_content = function() use ($ajax_response) {
-      return $ajax_response;
-    };
-    $this->assertSame($ajax_response, $this->ajaxController->content($request, $_content));
-  }
-
 }
 
-class TestAjaxController extends AjaxController {
+class TestAjaxRenderer extends AjaxRenderer {
 
   /**
    * {@inheritdoc}
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.libraries.yml b/core/themes/bartik/bartik.libraries.yml
index 410c21c..6866d6b 100644
--- a/core/themes/bartik/bartik.libraries.yml
+++ b/core/themes/bartik/bartik.libraries.yml
@@ -14,3 +14,4 @@ maintenance_page:
       css/maintenance-page.css: {}
   dependencies:
     - system/maintenance
+    - bartik/base
diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme
index 60f9a34..3216ab2 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';
+    $variables['attributes']['class'][] = 'layout-two-sidebars';
   }
   elseif (!empty($variables['page']['sidebar_first'])) {
-    $classes[] = 'layout-one-sidebar';
-    $classes[] = 'layout-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';
+    $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';
+    $variables['attributes']['class'][] = 'featured';
   }
 
   if (!empty($variables['page']['triptych_first'])
     || !empty($variables['page']['triptych_middle'])
     || !empty($variables['page']['triptych_last'])) {
-    $classes[] = 'triptych';
+    $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';
+    $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..75820cb 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 {
+.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');
-      }
-    }
-  }
-}
