diff --git a/core/core.services.yml b/core/core.services.yml
index b7c686a..6ac5a73 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -636,7 +636,7 @@ services:
       - { name: event_subscriber }
   controller.page:
     class: Drupal\Core\Controller\HtmlPageController
-    arguments: ['@controller_resolver', '@title_resolver', '@url_generator']
+    arguments: ['@controller_resolver', '@title_resolver', '@render_html_renderer']
   controller.ajax:
     class: Drupal\Core\Controller\AjaxController
     arguments: ['@controller_resolver', '@ajax_response_renderer']
@@ -662,6 +662,9 @@ services:
     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']
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
index eb4f140..277a3a4 100644
--- a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -7,9 +7,8 @@
 
 namespace Drupal\Core\Controller;
 
-use Drupal\Core\Page\FeedLinkElement;
 use Drupal\Core\Page\HtmlFragment;
-use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Page\RenderHtmlRendererInterface;
 use Drupal\Core\Utility\Title;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -23,28 +22,28 @@ class HtmlControllerBase {
   /**
    * The title resolver.
    *
-   * @var \Drupal\Core\Controller\TitleResolver
+   * @var \Drupal\Core\Controller\TitleResolverInterface
    */
   protected $titleResolver;
 
   /**
-   * The url generator.
+   * The render array to HTML fragment converter.
    *
-   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   * @var \Drupal\Core\Page\RenderHtmlRendererInterface
    */
-  protected $urlGenerator;
+  protected $renderHtmlRenderer;
 
   /**
    * Constructs a new HtmlControllerBase object.
    *
    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
    *   The title resolver.
-   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
-   *   The url generator.
+   * @param \Drupal\Core\Page\RenderHtmlRendererInterface $render_html_renderer
+   *   The render html renderer.
    */
-  public function __construct(TitleResolverInterface $title_resolver, UrlGeneratorInterface $url_generator) {
+  public function __construct(TitleResolverInterface $title_resolver, RenderHtmlRendererInterface $render_html_renderer) {
     $this->titleResolver = $title_resolver;
-    $this->urlGenerator = $url_generator;
+    $this->renderHtmlRenderer = $render_html_renderer;
   }
 
   /**
@@ -65,47 +64,16 @@ protected function createHtmlFragment($page_content, Request $request) {
     }
 
     if (!is_array($page_content)) {
-      $page_content = array(
-        'main' => array(
-          '#markup' => $page_content,
-        ),
-      );
+      $page_content = ['#markup' => $page_content];
     }
 
-    $content = $this->drupalRender($page_content);
-    if (!empty($page_content)) {
-      drupal_process_attached($page_content);
-    }
-    $cache = !empty($page_content['#cache']['tags']) ? array('tags' => $page_content['#cache']['tags']) : array();
-    $fragment = new HtmlFragment($content, $cache);
+    $fragment = $this->renderHtmlRenderer->render($page_content);
 
-    // A title defined in the return always wins.
-    if (isset($page_content['#title'])) {
-      $fragment->setTitle($page_content['#title'], Title::FILTER_XSS_ADMIN);
-    }
-    else if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
+    if (!$fragment->getTitle() && $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
       $fragment->setTitle($this->titleResolver->getTitle($request, $route), Title::PASS_THROUGH);
     }
 
-    // Add feed links from the page content.
-    $attached = isset($page_content['#attached']) ? $page_content['#attached'] : array();
-    if (!empty($attached['drupal_add_feed'])) {
-      foreach ($attached['drupal_add_feed'] as $feed) {
-        $feed_link = new FeedLinkElement($feed[1], $this->urlGenerator->generateFromPath($feed[0]));
-        $fragment->addLinkElement($feed_link);
-      }
-    }
-
     return $fragment;
   }
 
-  /**
-   * Wraps drupal_render().
-   *
-   * @todo: Remove as part of https://drupal.org/node/2182149
-   */
-  protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
-    return drupal_render($elements, $is_recursive_call);
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php
index 5560c76..83d5658 100644
--- a/core/lib/Drupal/Core/Controller/HtmlPageController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\Core\Controller;
 
-use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Page\RenderHtmlRendererInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -29,11 +29,11 @@ class HtmlPageController extends HtmlControllerBase {
    *   The controller resolver.
    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
    *   The title resolver.
-   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
-   *   The url generator.
+   * @param \Drupal\Core\Page\RenderHtmlRendererInterface $render_html_renderer
+   *   The render html renderer.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver, UrlGeneratorInterface $url_generator) {
-    parent::__construct($title_resolver, $url_generator);
+  public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver, RenderHtmlRendererInterface $render_html_renderer) {
+    parent::__construct($title_resolver, $render_html_renderer);
 
     $this->controllerResolver = $controller_resolver;
   }
diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php b/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php
new file mode 100644
index 0000000..f3c166c
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/RenderHtmlRenderer.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\RenderHtmlRenderer.
+ */
+
+namespace Drupal\Core\Page;
+
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Utility\Title;
+
+/**
+ * Provides an implementation for an render array to HTML fragment renderer.
+ *
+ * This renderer takes into account the cache information, the attached assets
+ * as well as the title and HTML HEAD elements.
+ */
+class RenderHtmlRenderer implements RenderHtmlRendererInterface {
+
+  /**
+   * The URL generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * Constructs a new RenderHtmlRenderer.
+   *
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   *   The URL generator.
+   */
+  public function __construct(UrlGeneratorInterface $url_generator) {
+    $this->urlGenerator = $url_generator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(array $render_array) {
+    $content = $this->drupalRender($render_array);
+    if (!empty($render_array)) {
+      drupal_process_attached($render_array);
+    }
+    $cache = !empty($render_array['#cache']['tags']) ? ['tags' => $render_array['#cache']['tags']] : [];
+    $fragment = new HtmlFragment($content, $cache);
+
+    if (isset($render_array['#title'])) {
+      $fragment->setTitle($render_array['#title'], Title::FILTER_XSS_ADMIN);
+    }
+
+    $attached = isset($render_array['#attached']) ? $render_array['#attached'] : [];
+    $attached += [
+      'drupal_add_feed' => [],
+      'drupal_add_html_head' => [],
+      'drupal_add_html_head_link' => [],
+    ];
+
+
+    // Add feed links from the page content.
+    foreach ($attached['drupal_add_feed'] as $feed) {
+      $fragment->addLinkElement(new FeedLinkElement($feed[1], $this->urlGenerator->generateFromPath($feed[0])));
+    }
+
+    // Add generic links from the page content.
+    foreach ($attached['drupal_add_html_head_link'] as $link) {
+      $fragment->addLinkElement(new LinkElement($this->urlGenerator->generateFromPath($link[0]['href']), $link[0]['rel']));
+    }
+
+    // @todo Also transfer the contents of "drupal_add_html_head" once
+    // https://www.drupal.org/node/2296951 lands.
+
+    // @todo Transfer CSS and JS over to the fragment once those are supported
+    // on the fragment object.
+
+    return $fragment;
+  }
+
+  /**
+   * Wraps drupal_render().
+   *
+   * @todo: Convert drupal_render into a proper injectable service.
+   */
+  protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
+    return drupal_render($elements, $is_recursive_call);
+  }
+}
diff --git a/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php b/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php
new file mode 100644
index 0000000..95fabae
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/RenderHtmlRendererInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\RenderHtmlRendererInterface.
+ */
+
+namespace Drupal\Core\Page;
+
+/**
+ * Interface for a render array to HTML fragment renderer.
+ *
+ * An HTML Fragment Renderer is responsible for translating a Drupal render
+ * array into an HtmlFragmentInterface object.
+ */
+interface RenderHtmlRendererInterface {
+
+  /**
+   * Converts a render array into a corresponding HtmlFragment object.
+   *
+   * @param array $render_array
+   *   The render array to convert.
+   *
+   * @return \Drupal\Core\Page\HtmlFragment
+   *   The equivalent HtmlFragment object.
+   */
+  public function render(array $render_array);
+
+}
