diff --git a/core/core.services.yml b/core/core.services.yml
index 5399bc7..8accbc1 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -350,7 +350,12 @@ services:
       - { name: event_subscriber }
   controller.page:
     class: Drupal\Core\Controller\HtmlPageController
-    arguments: ['@http_kernel', '@controller_resolver']
+    arguments: ['@controller_resolver']
+  controller.form:
+    class: Drupal\Core\Controller\HtmlFormController
+    arguments: ['@controller_resolver']
+    calls:
+      - [setContainer, ['@service_container']]
   controller.dialog:
     class: Drupal\Core\Controller\DialogController
     arguments: ['@http_kernel']
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 0dbabd8..f50937f 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3630,7 +3630,7 @@ function drupal_pre_render_dropbutton($element) {
 }
 
 /**
- * Renders the page, including all theming.
+ * 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
@@ -3640,10 +3640,17 @@ function drupal_pre_render_dropbutton($element) {
  *   - #show_messages: Suppress drupal_get_message() items. Used by Batch
  *     API (optional).
  *
+ * @param boolean $legacy
+ *   TRUE if this function should use the old-style page element that includes
+ *   the HTML wrapper. FALSE to use the page element that doesn't self-render
+ *   the HTML part of the page. Defaults to false.
+ * @return array
+ *   The processed render array for the page.
+ *
  * @see hook_page_alter()
  * @see element_info()
  */
-function drupal_render_page($page) {
+function drupal_prepare_page($page, $legacy = FALSE) {
   $main_content_display = &drupal_static('system_main_content_added', FALSE);
 
   // Pull out the page title to set it back later.
@@ -3656,7 +3663,7 @@ function drupal_render_page($page) {
   // 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');
+    $page = $legacy ? element_info('page') : element_info('page_body');
   }
 
   // Modules can add elements to $page as needed in hook_page_build().
@@ -3680,6 +3687,25 @@ function drupal_render_page($page) {
     $page['#title'] = $title;
   }
 
+  return $page;
+}
+
+/**
+ * Renders the page, including all theming.
+ *
+ * @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).
+ *
+ * @see hook_page_alter()
+ * @see element_info()
+ */
+function drupal_render_page($page) {
+  $page = drupal_prepare_page($page, TRUE);
   return drupal_render($page);
 }
 
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 3a236e4..2bee1fb 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -19,6 +19,9 @@
 use Drupal\Core\Theme\ThemeSettings;
 use Drupal\Component\Utility\NestedArray;
 
+use Drupal\Core\Page\Link;
+use Drupal\Core\Page\Metatag;
+
 /**
  * @defgroup content_flags Content markers
  * @{
@@ -2494,6 +2497,109 @@ function _template_preprocess_default_variables() {
  *
  * @see system_elements()
  */
+function template_preprocess_html_page(&$variables) {
+
+  /** @var \Drupal\Core\Page\HtmlPage */
+  $page = $variables['page_object'];
+
+  $variables['html_attributes'] = $page->getHtmlAttributes();
+  $variables['attributes'] = $page->getAttributes();
+  $variables['page'] = $page->getContent();
+
+  // 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'];
+  $body_classes[] = 'html';
+  // Add a class that tells us whether we're on the front page or not.
+  $body_classes[] = $variables['is_front'] ? 'front' : 'not-front';
+  // Add a class that tells us whether the page is viewed by an authenticated user or not.
+  $body_classes[] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in';
+  $variables['attributes']['class'] = $body_classes;
+
+  // Populate the body classes.
+  if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) {
+    foreach ($suggestions as $suggestion) {
+      if ($suggestion != 'page-front') {
+        // Add current suggestion to page classes to make it possible to theme
+        // the page depending on the current page type (e.g. node, admin, user,
+        // etc.) as well as more specific data like node-12 or node-edit.
+        $variables['attributes']['class'][] = drupal_html_class($suggestion);
+      }
+    }
+  }
+
+  // If on an individual node page, add the node type to body classes.
+  if ($node = menu_get_object()) {
+    $variables['attributes']['class'][] = drupal_html_class('node-type-' . $node->type);
+  }
+
+  // Add favicon.
+  if (theme_get_setting('features.favicon')) {
+    $favicon = theme_get_setting('favicon.url');
+    $type = theme_get_setting('favicon.mimetype');
+    $link = new Link(drupal_strip_dangerous_protocols($favicon), 'shortcut icon');
+    $link->setAttribute('type', $type);
+    $page->addLink($link);
+  }
+
+  $site_config = Drupal::config('system.site');
+  if ($page->hasTitle()) {
+    $head_title = array(
+      'title' => strip_tags($page->getTitle()),
+      'name' => String::checkPlain($site_config->get('name')),
+    );
+  }
+  else {
+    $head_title = array('name' => String::checkPlain($site_config->get('name')));
+    if ($site_config->get('slogan')) {
+      $head_title['slogan'] = strip_tags(Xss::filterAdmin($site_config->get('slogan')));
+    }
+  }
+
+  $variables['head_title_array'] = $head_title;
+  $variables['head_title'] = implode(' | ', $head_title);
+
+  // Display the html.tpl.php's default mobile metatags for responsive design.
+  $page->addMetatag(new Metatag('MobileOptimized', 'width'));
+  $page->addMetatag(new Metatag('HandheldFriendly', 'true'));
+  $page->addMetatag(new Metatag('viewport', 'width=device-width'));
+
+  // Populate the page template suggestions.
+  if ($suggestions = theme_get_suggestions(arg(), 'html')) {
+    $variables['theme_hook_suggestions'] = $suggestions;
+  }
+
+  drupal_add_library('system', 'html5shiv', TRUE);
+
+  $variables['page_top'][] = array('#markup' => $page->getContentTop());
+  $variables['page_bottom'][] = array('#markup' => $page->getContentBottom());
+
+  // Add footer scripts as '#markup' so they can be rendered with other
+  // elements in page_bottom.
+  $footer_scripts = new RenderWrapper('drupal_get_js', array('footer'));
+  $variables['page_bottom'][] = array('#markup' => $footer_scripts);
+
+  // Wrap function calls in an object so they can be called when printed.
+  $variables['head'] = new RenderWrapper(function() use ($page) {
+    return implode("\n", $page->metatags()) . implode("\n", $page->links());
+  });
+  $variables['styles'] = new RenderWrapper('drupal_get_css');
+  $variables['scripts'] = new RenderWrapper('drupal_get_js');
+}
+
+/**
+ * Prepares variables for HTML document templates.
+ *
+ * This is specifically for the legacy router only.
+ *
+ * Default template: html.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - page: A render element representing the page.
+ *
+ * @see system_elements()
+ */
 function template_preprocess_html(&$variables) {
   $language_interface = language(Language::TYPE_INTERFACE);
 
@@ -3000,9 +3106,14 @@ function drupal_common_theme() {
       'render element' => 'page',
       'template' => 'html',
     ),
+    'html_page' => array(
+      'variables' => array('page_object' => NULL),
+      'template' => 'html',
+    ),
     'page' => array(
       'render element' => 'page',
       'template' => 'page',
+      'theme wrapper' => 'html',
     ),
     'region' => array(
       'render element' => 'elements',
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index 8515af0..eb562a2 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -13,13 +13,16 @@
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
 use Symfony\Component\HttpKernel\Exception\FlattenException;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Page\HtmlPage;
 
 /**
  * This controller handles HTTP errors generated by the routing system.
  */
-class ExceptionController extends ContainerAware {
+class ExceptionController extends HtmlControllerBase implements ContainerAwareInterface {
 
   /**
    * The content negotiation library.
@@ -29,6 +32,24 @@ class ExceptionController extends ContainerAware {
   protected $negotiation;
 
   /**
+   * The service container.
+   *
+   * @var ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * Sets the Container associated with this Controller.
+   *
+   * @param ContainerInterface $container A ContainerInterface instance
+   *
+   * @api
+   */
+  public function setContainer(ContainerInterface $container = null) {
+    $this->container = $container;
+  }
+
+  /**
    * Constructor.
    *
    * @param Drupal\Core\ContentNegotiation $negotiation
@@ -84,7 +105,8 @@ public function on403Html(FlattenException $exception, Request $request) {
     $system_path = $request->attributes->get('_system_path');
     watchdog('access denied', $system_path, NULL, WATCHDOG_WARNING);
 
-    $path = $this->container->get('path.alias_manager')->getSystemPath(\Drupal::config('system.site')->get('page.403'));
+    $system_config = $this->container->get('config.factory')->get('system.site');
+    $path = $this->container->get('path.alias_manager')->getSystemPath($system_config->get('page.403'));
     if ($path && $path != $system_path) {
       // Keep old path for reference, and to allow forms to redirect to it.
       if (!isset($_GET['destination'])) {
@@ -113,15 +135,18 @@ public function on403Html(FlattenException $exception, Request $request) {
       $response->setStatusCode(403, 'Access denied');
     }
     else {
+      $page_content = array(
+        '#markup' => t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())),
+      );
+
+      $page = $this->createHtmlPage($page_content, $request);
+      $page->setTitle(t('Page not found'));
+      $page->setStatusCode(404);
 
-      // @todo Replace this block with something cleaner.
-      $return = t('You are not authorized to access this page.');
-      drupal_set_title(t('Access denied'));
-      drupal_set_page_content($return);
-      $page = element_info('page');
-      $content = drupal_render_page($page);
+      $output = theme('html_page', array('page_object' => $page));
+      $response = new Response($output, $page->getStatusCode());
 
-      $response = new Response($content, 403);
+      return $response;
     }
 
     return $response;
@@ -186,17 +211,19 @@ public function on404Html(FlattenException $exception, Request $request) {
       $response->setStatusCode(404, 'Not Found');
     }
     else {
-      // @todo Replace this block with something cleaner.
-      $return = t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo()));
-      drupal_set_title(t('Page not found'));
-      drupal_set_page_content($return);
-      $page = element_info('page');
-      $content = drupal_render_page($page);
-
-      $response = new Response($content, 404);
-    }
+      $page_content = array(
+        '#markup' => t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())),
+      );
 
-    return $response;
+      $page = $this->createHtmlPage($page_content, $request);
+      $page->setTitle(t('Page not found'));
+      $page->setStatusCode(404);
+
+      $output = theme('html_page', array('page_object' => $page));
+      $response = new Response($output, $page->getStatusCode());
+
+      return $response;
+    }
   }
 
   /**
@@ -252,17 +279,17 @@ public function on500Html(FlattenException $exception, Request $request) {
       drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class);
     }
 
-    drupal_set_title(t('Error'));
-    // We fallback to a maintenance page at this point, because the page
-    // generation itself can generate errors.
-    $maintenance_page = array(
+    $page_content = array(
       '#theme' => 'maintenance_page',
       '#content' => t('The website has encountered an error. Please try again later.'),
     );
-    $output = drupal_render($maintenance_page);
 
-    $response = new Response($output, 500);
-    $response->setStatusCode(500, '500 Service unavailable (with message)');
+    $page = $this->createHtmlPage($page_content, $request);
+    $page->setTitle(t('Error'));
+    $page->setStatusCode(500);
+
+    $output = drupal_render($page_content);
+    $response = new Response($output, $page->getStatusCode());
 
     return $response;
   }
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
new file mode 100644
index 0000000..25833d2
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Core\Controller;
+
+use Symfony\Component\HttpFoundation\Request;
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Page\HtmlPage;
+
+/**
+ * Base class for Html page-generating controllers.
+ */
+class HtmlControllerBase {
+
+  /**
+   * Converts a render array into an HtmlPage object.
+   *
+   * @param array|string $page_content
+   *   The page content area to display.
+   * @param \Drupal\Core\Controller\Request $request
+   *   The request object.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   A page object.
+   */
+  public function createHtmlPage($page_content, Request $request) {
+    if ($page_content instanceof HtmlPage || $page_content instanceof Response) {
+      return $page_content;
+    }
+
+    if (!is_array($page_content)) {
+      $page_content = array(
+        '#markup' => $page_content,
+      );
+    }
+
+    $page = new HtmlPage('', $request->attributes->get('_title'));
+
+    $page_array = drupal_prepare_page($page_content);
+
+    $page = $this->preparePage($page, $page_array);
+
+    $page->setContentTop(drupal_render($page_array['page_top']));
+    $page->setContentBottom(drupal_render($page_array['page_bottom']));
+    $page->setContent(drupal_render($page_array));
+
+    return $page;
+  }
+
+  /**
+   * Enhances a page object based on a render array.
+   *
+   * @param \Drupal\Core\Page\HtmlPage $page
+   *   The page object to enhance.
+   * @param array $page_array
+   *   The page array to extract onto the page object.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The modified page object.
+   */
+  protected function preparePage(HtmlPage $page, $page_array) {
+    if (isset($page_array['#title'])) {
+      $page->setTitle($page_array['#title']);
+      unset($page_array['#title']);
+    }
+    elseif ($title = drupal_get_title()) {
+      $page->setTitle($title);
+    }
+
+    $attributes = $page->getAttributes();
+
+    // Add information about the number of sidebars.
+    $classes = $attributes['class'];
+    if (!empty($page_array['sidebar_first']) && !empty($page_array['page']['sidebar_second'])) {
+      $classes[] = 'two-sidebars';
+    }
+    elseif (!empty($page_array['sidebar_first'])) {
+      $classes[] = 'one-sidebar';
+      $classes[] = 'sidebar-first';
+    }
+    elseif (!empty($page_array['sidebar_second'])) {
+      $classes[] = 'one-sidebar';
+      $classes[] = 'sidebar-second';
+    }
+    else {
+      $classes[] = 'no-sidebars';
+    }
+    $attributes['class'] = $classes;
+
+    // HTML element attributes.
+    $language_interface = language(Language::TYPE_INTERFACE);
+    $html_attributes = $page->getHtmlAttributes();
+    $html_attributes['lang'] = $language_interface->id;
+    $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
+
+    return $page;
+  }
+
+}
+
diff --git a/core/lib/Drupal/Core/Controller/HtmlFormController.php b/core/lib/Drupal/Core/Controller/HtmlFormController.php
index 8e88b53..ce3592a 100644
--- a/core/lib/Drupal/Core/Controller/HtmlFormController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlFormController.php
@@ -15,7 +15,7 @@
 /**
  * Wrapping controller for forms that serve as the main page body.
  */
-class HtmlFormController implements ContainerAwareInterface {
+class HtmlFormController extends HtmlPageController implements ContainerAwareInterface {
 
   /**
    * The injection container for this object.
@@ -46,7 +46,12 @@ public function setContainer(ContainerInterface $container = NULL) {
    *   A response object.
    */
   public function content(Request $request, $_form) {
-    $form_object = $this->getFormObject($request, $_form);
+    $page_content = $this->getContentResult($request, $_form);
+    return $this->createHtmlPage($page_content, $request);
+  }
+
+  public function getContentResult(Request $request, $controller_definition) {
+    $form_object = $this->getFormObject($request, $controller_definition);
 
     // Using reflection, find all of the parameters needed by the form in the
     // request attributes, skipping $form and $form_state.
@@ -66,6 +71,12 @@ public function content(Request $request, $_form) {
 
     $form_id = _drupal_form_id($form_object, $form_state);
     return drupal_build_form($form_id, $form_state);
+
+    $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition);
+    $arguments = $this->controllerResolver->getArguments($request, $callable);
+    $page_content = call_user_func_array($callable, $arguments);
+
+    return $page_content;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php
index d87b3ed..cf1a35b 100644
--- a/core/lib/Drupal/Core/Controller/HtmlPageController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php
@@ -11,17 +11,11 @@
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
 
+
 /**
  * Default controller for most HTML pages.
  */
-class HtmlPageController {
-
-  /**
-   * The HttpKernel object to use for subrequests.
-   *
-   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
-   */
-  protected $httpKernel;
+class HtmlPageController extends HtmlControllerBase {
 
   /**
    * The controller resolver.
@@ -37,8 +31,7 @@ class HtmlPageController {
    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
    *   The controller resolver.
    */
-  public function __construct(HttpKernelInterface $kernel, ControllerResolverInterface $controller_resolver) {
-    $this->httpKernel = $kernel;
+  public function __construct(ControllerResolverInterface $controller_resolver) {
     $this->controllerResolver = $controller_resolver;
   }
 
@@ -54,24 +47,16 @@ public function __construct(HttpKernelInterface $kernel, ControllerResolverInter
    *   A response object.
    */
   public function content(Request $request, $_content) {
-    $callable = $this->controllerResolver->getControllerFromDefinition($_content);
+    $page_content = $this->getContentResult($request, $_content);
+    return $this->createHtmlPage($page_content, $request);
+  }
+
+  public function getContentResult(Request $request, $controller_definition) {
+    $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition);
     $arguments = $this->controllerResolver->getArguments($request, $callable);
     $page_content = call_user_func_array($callable, $arguments);
-    if ($page_content instanceof Response) {
-      return $page_content;
-    }
-    if (!is_array($page_content)) {
-      $page_content = array(
-        '#markup' => $page_content,
-      );
-    }
-    // If no title was returned fall back to one defined in the route.
-    if (!isset($page_content['#title']) && $request->attributes->has('_title')) {
-      $page_content['#title'] = $request->attributes->get('_title');
-    }
 
-    $response = new Response(drupal_render_page($page_content));
-    return $response;
+    return $page_content;
   }
 
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
index dca2708..40892b8 100644
--- a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
@@ -11,6 +11,9 @@
 use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
+use Drupal\Core\Language\Language;
+use Drupal\Core\Page\HtmlPage;
+
 /**
  * Access subscriber for controller requests.
  */
@@ -43,7 +46,52 @@ public function onKernelControllerLegacy(FilterControllerEvent $event) {
     if ($request->attributes->get('_legacy')) {
       $router_item = $request->attributes->get('_drupal_menu_item');
       $new_controller = function() use ($router_item) {
-        return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
+        $page_content = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
+
+        $page = new HtmlPage();
+
+        $page_array = drupal_prepare_page($page_content);
+
+        //$page = $this->preparePage($page, $page_array);
+        if (isset($page_array['#title'])) {
+          $page->setTitle($page_array['#title']);
+          unset($page_array['#title']);
+        }
+        elseif ($title = drupal_get_title()) {
+          $page->setTitle($title);
+        }
+
+        $attributes = $page->getAttributes();
+
+        // Add information about the number of sidebars.
+        $classes = $attributes['class'];
+        if (!empty($page_array['sidebar_first']) && !empty($page_array['page']['sidebar_second'])) {
+          $classes[] = 'two-sidebars';
+        }
+        elseif (!empty($page_array['sidebar_first'])) {
+          $classes[] = 'one-sidebar';
+          $classes[] = 'sidebar-first';
+        }
+        elseif (!empty($page_array['sidebar_second'])) {
+          $classes[] = 'one-sidebar';
+          $classes[] = 'sidebar-second';
+        }
+        else {
+          $classes[] = 'no-sidebars';
+        }
+        $attributes['class'] = $classes;
+
+        // HTML element attributes.
+        $language_interface = language(Language::TYPE_INTERFACE);
+        $html_attributes = $page->getHtmlAttributes();
+        $html_attributes['lang'] = $language_interface->id;
+        $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
+
+        $page->setContentTop(drupal_render($page_array['page_top']));
+        $page->setContentBottom(drupal_render($page_array['page_bottom']));
+        $page->setContent(drupal_render($page_array));
+
+        return $page;
       };
       $event->setController($new_controller);
     }
diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
index fa4be98..ad05312 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
@@ -15,6 +15,7 @@
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Page\HtmlPage;
 
 /**
  * Main subscriber for VIEW HTTP responses.
@@ -32,6 +33,23 @@ public function __construct(ContentNegotiation $negotiation) {
   }
 
   /**
+   * Renders an HtmlPage object to a Response.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
+   *   The Event to process.
+   */
+  public function onHtmlPage(GetResponseForControllerResultEvent $event) {
+    $page = $event->getControllerResult();
+    if ($page instanceof HtmlPage && $event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
+      $output = theme('html_page', array('page_object' => $page));
+
+      $response = new Response($output, $page->getStatusCode());
+
+      $event->setResponse($response);
+    }
+  }
+
+  /**
    * Processes a successful controller into an HTTP 200 response.
    *
    * Some controllers may not return a response object but simply the body of
@@ -62,17 +80,6 @@ public function onView(GetResponseForControllerResultEvent $event) {
         $event->setResponse(new Response('Not Acceptable', 406));
       }
     }
-    elseif ($request->attributes->get('_legacy')) {
-      // This is an old hook_menu-based subrequest, which means we assume
-      // the body is supposed to be the complete page.
-      $page_result = $event->getControllerResult();
-      if (!is_array($page_result)) {
-        $page_result = array(
-          '#markup' => $page_result,
-        );
-      }
-      $event->setResponse(new Response(drupal_render_page($page_result)));
-    }
     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
@@ -141,8 +148,8 @@ public function onIframeUpload(GetResponseForControllerResultEvent $event) {
    *   The Event to process.
    */
   public function onHtml(GetResponseForControllerResultEvent $event) {
-    $page_callback_result = $event->getControllerResult();
-    return new Response(drupal_render_page($page_callback_result));
+    //$page_callback_result = $event->getControllerResult();
+    //return new Response(drupal_render_page($page_callback_result));
   }
 
   /**
@@ -152,6 +159,7 @@ public function onHtml(GetResponseForControllerResultEvent $event) {
    *   An array of event listener definitions.
    */
   static function getSubscribedEvents() {
+    $events[KernelEvents::VIEW][] = array('onHtmlPage', 10);
     $events[KernelEvents::VIEW][] = array('onView');
 
     return $events;
diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php
new file mode 100644
index 0000000..ce28d53
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HeadElement.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file Contains Drupal\Core\Page\HeadElement.
+ */
+
+namespace Drupal\Core\Page;
+
+/**
+ * This class represents an HTML element that appears in the HEAD tag.
+ */
+class HeadElement {
+
+  /**
+   * An array of attributes for this element.
+   *
+   * @var array
+   */
+  protected $attributes = array();
+
+  /**
+   * The element name.
+   *
+   * Sub-classes should override this value with the name of their element.
+   *
+   * @var string
+   */
+  protected $element = '';
+
+  /**
+   * Renders this object to an HTML element string.
+   *
+   * @return string
+   */
+  public function __toString() {
+    $attributes = array_filter($this->attributes);
+
+    $rendered = '';
+    foreach ($attributes as $key => $value) {
+      $rendered .= " $key=\"{$value}\"";
+    }
+
+    return "<{$this->element}{$rendered} />";
+  }
+
+  /**
+   * Sets an attribute on this element.
+   *
+   * @param type $key
+   *   The attribute to set.
+   * @param type $value
+   *   The value to which to set it.
+   */
+  public function setAttribute($key, $value) {
+    $this->attributes[$key] = $value;
+  }
+}
diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php
new file mode 100644
index 0000000..abf99a5
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HtmlFragment.php
@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\HtmlFragment.
+ */
+
+namespace Drupal\Core\Page;
+
+use Drupal\Core\Asset\AssetBag;
+use Drupal\Core\Asset\AssetBagInterface;
+use Drupal\Core\Utility\Title;
+
+/**
+ * Response object that contains variables for injection into the html template.
+ *
+ * @todo should we have this conform to an interface? https://drupal.org/node/1871596#comment-7134686
+ * @todo add method replacements for *all* data sourced by html.tpl.php
+ */
+class HtmlFragment {
+
+  /**
+   * HTML content string.
+   *
+   * @var string
+   */
+  protected $content;
+
+  /**
+   * The title of this HtmlFragment.
+   *
+   * @var string
+   */
+  protected $title = '';
+
+  public function __construct($content = '') {
+    $this->content = $content;
+    //$this->bag = new AssetBag();
+  }
+
+  /**
+   * Adds another asset bag to those already contained in this fragment.
+   *
+   * @param AssetBagInterface $bag
+   */
+  public function addAssetBag(AssetBagInterface $bag) {
+    $this->bag->addAssetBag($bag);
+  }
+
+  /**
+   * Returns the AssetBag representing the collected assets in this fragment.
+   *
+   * @return AssetBag
+   */
+  public function getAssets() {
+    return $this->bag;
+  }
+
+  /**
+   * Sets the response content.
+   *
+   * This should be the bulk of the page content, and will ultimately be placed
+   * within the <body> tag in final HTML output.
+   *
+   * Valid types are strings, numbers, and objects that implement a __toString()
+   * method.
+   *
+   * @param mixed $content
+   *
+   * @return \Drupal\Core\Page\HtmlFragment
+   */
+  public function setContent($content) {
+    $this->content = $content;
+    return $this;
+  }
+
+  /**
+   * Gets the main content of this HtmlFragment.
+   *
+   * @return string
+   */
+  public function getContent() {
+    return $this->content;
+  }
+
+  /**
+   * Sets the title of this HtmlFragment.
+   *
+   * Handling of this title varies depending on what is consuming this
+   * HtmlFragment object. If it's a block, it may only be used as the block's
+   * title; if it's at the page level, it will be used in a number of places,
+   * including the html <head> title.
+   */
+  public function setTitle($title, $output = Title::CHECK_PLAIN) {
+    $this->title = ($output == PASS_THROUGH) ? $title : check_plain($title);
+  }
+
+  /**
+   * Indicates whether or not this HtmlFragment has a title.
+   *
+   * @return bool
+   */
+  public function hasTitle() {
+    return !empty($this->title);
+  }
+
+  /**
+   * Gets the title for this HtmlFragment, if any.
+   *
+   * @return string
+   */
+  public function getTitle() {
+    return $this->title;
+  }
+
+  /**
+   * Clones the current HtmlFragment instance.
+   */
+  public function __clone() {
+    $this->bag = clone $this->bag;
+  }
+}
diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php
new file mode 100644
index 0000000..635ef11
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/HtmlPage.php
@@ -0,0 +1,212 @@
+<?php
+
+/**
+ * @file Contains Drupal\Core\Page\HtmlPage.
+ */
+
+namespace Drupal\Core\Page;
+
+use Drupal\Core\Template\Attribute;
+
+/**
+ * Data object for an HTML page.
+ *
+ */
+class HtmlPage extends HtmlFragment {
+
+  /**
+   * An array of Link elements.
+   * @var array
+   */
+  protected $links = array();
+
+  /**
+   * An array of Meta elements.
+   *
+   * @var array
+   */
+  protected $metatags = array();
+
+  /**
+   * Attributes for the HTML element.
+   *
+   * @var \Drupal\Core\Template\Attribute
+   */
+  protected $htmlAttributes;
+
+  /**
+   * Attributes for the BODY element.
+   *
+   * @var \Drupal\Core\Template\Attribute
+   */
+  protected $bodyAttributes;
+
+  /**
+   * Auxiliary content area, above the main content.
+   *
+   * @var string
+   */
+  protected $contentTop = '';
+
+  /**
+   * Auxiliary content area, below the main content.
+   *
+   * @var string
+   */
+  protected $contentBottom = '';
+
+  /**
+   * The HTTP status code of this page.
+   *
+   * @var int
+   */
+  protected $statusCode = 200;
+
+  /**
+   * Constructs a new HtmlPage object.
+   *
+   * @param string $content
+   *   (Optional) The body content of the page.
+   * @param string $title
+   *   (Optional) The title of the page.
+   */
+  public function __construct($content = '', $title = '') {
+    parent::__construct($content);
+
+    $this->title = $title;
+
+    $this->htmlAttributes = new Attribute();
+    $this->bodyAttributes = new Attribute();
+  }
+
+  /**
+   * Adds a Link to the page.
+   *
+   * @param \Drupal\Core\Page\Link $link
+   *   A link element to enqueue.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The invoked object.
+   */
+  public function addLink(Link $link) {
+    $this->links[] = $link;
+    return $this;
+  }
+
+  /**
+   * Returns an array of all enqueued links.
+   *
+   * @return array
+   */
+  public function links() {
+    return $this->links;
+  }
+
+  /**
+   * Adds a Meta tag to the page.
+   *
+   * @param \Drupal\Core\Page\Metatag $tag
+   *   A meta element to add.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The invoked object.
+   */
+  public function addMetatag(Metatag $tag) {
+    $this->metatags[] = $tag;
+    return $this;
+  }
+
+  /**
+   * Returns an array of all enqueued meta tags.
+   * @return array
+   */
+  public function metatags() {
+    return $this->metatags;
+  }
+
+  /**
+   * Returns the HTML attributes for this HTML page.
+   *
+   * @return \Drupal\Core\Template\Attribute
+   */
+  public function getHtmlAttributes() {
+    return $this->htmlAttributes;
+  }
+
+  /**
+   * Returns the HTML attributes for the body element of this page.
+   *
+   * @return \Drupal\Core\Template\Attribute
+   */
+  public function getAttributes() {
+    return $this->bodyAttributes;
+  }
+
+  /**
+   * Sets the top-content of this page.
+   *
+   * @param string $content
+   *   The top-content to set.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The called object.
+   */
+  public function setContentTop($content) {
+    $this->contentTop = $content;
+    return $this;
+  }
+
+  /**
+   * Returns the top-content of this page.
+   *
+   * @return string
+   *   The top-content of this page.
+   */
+  public function getContentTop() {
+    return $this->contentTop;
+  }
+
+  /**
+   * Sets the bottom-content of this page.
+   *
+   * @param string $content
+   *   The bottom-content to set.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The called object.
+   */
+  public function setContentBottom($content) {
+    $this->contentBottom = $content;
+    return $this;
+  }
+
+  /**
+   * Returns the bottom-content of this page.
+   *
+   * @return string
+   *   The bottom-content of this page.
+   */
+  public function getContentBottom() {
+    return $this->contentBottom;
+  }
+
+  /**
+   * Sets the HTTP status of this page.
+   *
+   * @param int $status
+   *   The status code to set.
+   * @return \Drupal\Core\Page\HtmlPage
+   *   The called object.
+   */
+  public function setStatusCode($status) {
+    $this->statusCode = $status;
+    return $this;
+  }
+
+  /**
+   * Returns the status code of this response.
+   *
+   * @return int
+   *   The status code of thise page.
+   */
+  public function getStatusCode() {
+    return $this->statusCode;
+  }
+}
+
diff --git a/core/lib/Drupal/Core/Page/Link.php b/core/lib/Drupal/Core/Page/Link.php
new file mode 100644
index 0000000..6b772b4
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/Link.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file Contains \Drupal\Core\Page\Link.
+ */
+
+namespace Drupal\Core\Page;
+
+class Link extends HeadElement {
+
+  protected $element = 'link';
+
+  /**
+   * Constructs a new Link object.
+   *
+   * @param string $href
+   *   The Link URI. The URI should already be processed to be a fully qualified
+   *   absolute link if necessary.
+   * @param string $rel
+   *   (optional) The link relationship. This is usually an IANA or
+   *   Microformat-defined string.
+   * @param array $attributes
+   *   (Optional) Additional attributes for this element.
+   */
+  public function __construct($href, $rel = '', array $attributes = array()) {
+    $this->attributes = $attributes + array(
+      'href' => $href,
+      'rel' => $rel,
+    );
+  }
+}
diff --git a/core/lib/Drupal/Core/Page/Metatag.php b/core/lib/Drupal/Core/Page/Metatag.php
new file mode 100644
index 0000000..c4822d8
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/Metatag.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file Contains \Drupal\Core\Page\Metatag.
+ */
+
+namespace Drupal\Core\Page;
+
+class Metatag extends HeadElement {
+
+  protected $element = 'meta';
+
+  /**
+   *
+   * @param string $name
+   *   The value of the name attribute.
+   * @param string $content
+   *   (Optional) The value of the content attribute.
+   * @param array $attributes
+   *   (Optional) Additional attributes for this element.
+   */
+  public function __construct($name, $content = '', array $attributes = array()) {
+    $this->attributes = $attributes + array(
+      'name' =>  $name,
+      'content' => $content,
+    );
+  }
+}
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php
index f84bb32..e9b49df 100644
--- a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php
+++ b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php
@@ -38,7 +38,7 @@ public function __construct(ContentNegotiation $negotiation) {
    */
   public function enhance(array $defaults, Request $request) {
     if (empty($defaults['_controller']) && !empty($defaults['_form']) && $this->negotiation->getContentType($request) === 'html') {
-      $defaults['_controller'] = '\Drupal\Core\Controller\HtmlFormController::content';
+      $defaults['_controller'] = 'controller.form:content';
     }
     return $defaults;
   }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index bd58f93..887f069 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -263,6 +263,10 @@ function system_element_info() {
     '#theme' => 'page',
     '#theme_wrappers' => array('html'),
   );
+  $types['page_body'] = array(
+    '#show_messages' => TRUE,
+    '#theme' => 'page',
+  );
   // By default, we don't want Ajax commands being rendered in the context of an
   // HTML page, so we don't provide defaults for #theme or #theme_wrappers.
   // However, modules can set these properties (for example, to provide an HTML
