diff --git a/core/core.services.yml b/core/core.services.yml
index 951c458..ffe4290 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -483,7 +483,7 @@ services:
       - { name: event_subscriber }
   controller.page:
     class: Drupal\Core\Controller\HtmlPageController
-    arguments: ['@controller_resolver', '@title_resolver']
+    arguments: ['@controller_resolver', '@title_resolver', '@url_generator']
   controller.ajax:
     class: Drupal\Core\Controller\AjaxController
     arguments: ['@controller_resolver', '@ajax_response_renderer']
@@ -609,7 +609,7 @@ services:
     arguments: ['@config.manager', '@config.storage', '@config.storage.snapshot']
   exception_controller:
     class: Drupal\Core\Controller\ExceptionController
-    arguments: ['@content_negotiation', '@title_resolver', '@html_page_renderer', '@html_fragment_renderer', '@string_translation']
+    arguments: ['@content_negotiation', '@title_resolver', '@html_page_renderer', '@html_fragment_renderer', '@string_translation', '@url_generator']
     calls:
       - [setContainer, ['@service_container']]
   exception_listener:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index fb370b8..cdb3d4b 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -236,12 +236,7 @@ function drupal_get_profile() {
  * @see drupal_pre_render_html_tag()
  */
 function drupal_add_html_head($data = NULL, $key = NULL) {
-  $stored_head = &drupal_static(__FUNCTION__);
-
-  if (!isset($stored_head)) {
-    // Make sure the defaults, including Content-Type, come first.
-    $stored_head = _drupal_default_html_head();
-  }
+  $stored_head = &drupal_static(__FUNCTION__, array());
 
   if (isset($data) && isset($key)) {
     if (!isset($data['#type'])) {
@@ -253,44 +248,23 @@ function drupal_add_html_head($data = NULL, $key = NULL) {
 }
 
 /**
- * Returns elements that are always displayed in the HEAD tag of the HTML page.
- */
-function _drupal_default_html_head() {
-  // Add default elements. 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.
-  $elements['system_meta_content_type'] = array(
-    '#type' => 'html_tag',
-    '#tag' => 'meta',
-    '#attributes' => array(
-      '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.
-  // Get the major version.
-  list($version, ) = explode('.', \Drupal::VERSION);
-  $elements['system_meta_generator'] = array(
-    '#type' => 'html_tag',
-    '#tag' => 'meta',
-    '#attributes' => array(
-      'name' => 'Generator',
-      'content' => 'Drupal ' . $version . ' (http://drupal.org)',
-    ),
-  );
-  // Also send the generator in the HTTP header.
-  $elements['system_meta_generator']['#attached']['drupal_add_http_header'][] = array('X-Generator', $elements['system_meta_generator']['#attributes']['content']);
-  return $elements;
-}
-
-/**
  * Retrieves output to be displayed in the HEAD tag of the HTML page.
+ *
+ * @param bool $render
+ *   If TRUE render the HEAD elements, otherwise return just the elements.
+ *
+ * @return string|array
+ *   Return the rendered HTML head or the elements itself.
  */
-function drupal_get_html_head() {
+function drupal_get_html_head($render = TRUE) {
   $elements = drupal_add_html_head();
   \Drupal::moduleHandler()->alter('html_head', $elements);
-  return drupal_render($elements);
+  if ($render) {
+    return drupal_render($elements);
+  }
+  else {
+    return $elements;
+  }
 }
 
 /**
@@ -307,35 +281,17 @@ function drupal_add_feed($url = NULL, $title = '') {
   $stored_feed_links = &drupal_static(__FUNCTION__, array());
 
   if (isset($url)) {
-    $feed_icon = array(
-      '#theme' => 'feed_icon',
-      '#url' => $url,
-      '#title' => $title,
-    );
-
-    $feed_icon['#attached']['drupal_add_html_head_link'][][] = array(
-      'rel' => 'alternate',
-      'type' => 'application/rss+xml',
-      'title' => $title,
-      // Force the URL to be absolute, for consistency with other <link> tags
-      // output by Drupal.
-      'href' => url($url, array('absolute' => TRUE)),
-    );
-
-    $stored_feed_links[$url] = drupal_render($feed_icon);
+    $stored_feed_links[$url] = array('url' => $url, 'title' => $title);
   }
   return $stored_feed_links;
 }
 
 /**
  * Gets the feed URLs for the current page.
- *
- * @param $delimiter
- *   A delimiter to split feeds by.
  */
-function drupal_get_feeds($delimiter = "\n") {
+function drupal_get_feeds() {
   $feeds = drupal_add_feed();
-  return implode($feeds, $delimiter);
+  return $feeds;
 }
 
 /**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 4230087..e8a0776 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -16,6 +16,9 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionNameLengthException;
+use Drupal\Core\Page\FeedLinkElement;
+use Drupal\Core\Page\LinkElement;
+use Drupal\Core\Page\MetaElement;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Template\RenderWrapper;
 use Drupal\Core\Theme\ThemeSettings;
@@ -2019,32 +2022,24 @@ function template_preprocess_html(&$variables) {
   $variables['head_title_array'] = $head_title;
   $variables['head_title'] = implode(' | ', $head_title);
 
-  // Display the html.html.twig's default mobile metatags for responsive design.
-  $elements = array(
-    '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',
-      ),
-    ),
-  );
-  foreach ($elements as $name => $element) {
-    drupal_add_html_head($element, $name);
+  // @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);
+    }
   }
 
   $variables['page_top'][] = array('#markup' => $page->getBodyTop());
@@ -2056,7 +2051,9 @@ function template_preprocess_html(&$variables) {
   $variables['page_bottom'][] = array('#markup' => $footer_scripts);
 
   // Wrap function calls in an object so they can be called when printed.
-  $variables['head'] = new RenderWrapper('drupal_get_html_head');
+  $variables['head'] = new RenderWrapper(function() use ($page) {
+    return implode("\n", $page->getMetaElements()) . implode("\n", $page->getLinkElements());
+  });
   $variables['styles'] = new RenderWrapper('drupal_get_css');
   $variables['scripts'] = new RenderWrapper('drupal_get_js');
 }
@@ -2111,7 +2108,27 @@ function template_preprocess_page(&$variables) {
     $variables['secondary_menu'] = theme_get_setting('features.secondary_menu') ? menu_secondary_menu() : array();
     $variables['action_links']   = menu_get_local_actions();
     $variables['tabs']           = menu_local_tabs();
-    $variables['feed_icons']     = drupal_get_feeds();
+
+    // Convert drupal_get_feeds to feed links on the page object.
+    /** @var \Drupal\Core\Page\HtmlPage $page */
+    $page = $variables['page']['#page'];
+    foreach (drupal_get_feeds() as $feed) {
+      // Force the URL to be absolute, for consistency with other <link> tags
+      // output by Drupal.
+      $link = new FeedLinkElement($feed['title'], url($feed['url'], array('absolute' => TRUE)));
+      $page->addLinkElement($link);
+    }
+    // Render the feed icons.
+    $variables['feed_icons'] = array();
+    foreach ($page->getLinkElements() as $link) {
+      if ($link instanceof FeedLinkElement) {
+        $variables['feed_icons'][] = array(
+          '#theme' => 'feed_icon',
+          '#url' => $link->getAttributes()['href'],
+          '#title' => $link->getAttributes()['title'],
+        );
+      }
+    }
   }
   else {
     $variables['main_menu']      = array();
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index eb3fe6c..f95930b 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -8,7 +8,9 @@
 namespace Drupal\Core\Controller;
 
 use Drupal\Core\Page\DefaultHtmlPageRenderer;
+use Drupal\Core\Page\HtmlFragmentRendererInterface;
 use Drupal\Core\Page\HtmlPageRendererInterface;
+use Drupal\Core\Routing\UrlGeneratorInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -68,9 +70,12 @@ class ExceptionController extends HtmlControllerBase implements ContainerAwareIn
    *   The page renderer.
    * @param \Drupal\Core\Page\HtmlFragmentRendererInterface $fragment_renderer
    *   The fragment rendering service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The url generator.
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
    */
-  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver, HtmlPageRendererInterface $renderer, $fragment_renderer, TranslationInterface $string_translation) {
-    parent::__construct($title_resolver);
+  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver, HtmlPageRendererInterface $renderer, HtmlFragmentRendererInterface $fragment_renderer, TranslationInterface $string_translation, UrlGeneratorInterface $url_generator) {
+    parent::__construct($title_resolver, $url_generator);
     $this->negotiation = $negotiation;
     $this->htmlPageRenderer = $renderer;
     $this->fragmentRenderer = $fragment_renderer;
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
index 5795a48..7789b57 100644
--- a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -7,7 +7,9 @@
 
 namespace Drupal\Core\Controller;
 
+use Drupal\Core\Page\FeedLinkElement;
 use Drupal\Core\Page\HtmlFragment;
+use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Utility\Title;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -26,13 +28,23 @@ class HtmlControllerBase {
   protected $titleResolver;
 
   /**
+   * The url generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
    * 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.
    */
-  public function __construct(TitleResolverInterface $title_resolver) {
+  public function __construct(TitleResolverInterface $title_resolver, UrlGeneratorInterface $url_generator) {
     $this->titleResolver = $title_resolver;
+    $this->urlGenerator = $url_generator;
   }
 
   /**
@@ -72,6 +84,15 @@ protected function createHtmlFragment($page_content, Request $request) {
       $fragment->setTitle($this->titleResolver->getTitle($request, $route), Title::PASS_THROUGH);
     }
 
+    // Add feed links from the page content.
+    $attached = drupal_render_collect_attached($page_content, TRUE);
+    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;
   }
 
diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php
index 6ed82f1..5560c76 100644
--- a/core/lib/Drupal/Core/Controller/HtmlPageController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Controller;
 
+use Drupal\Core\Routing\UrlGeneratorInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -28,9 +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.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver) {
-    parent::__construct($title_resolver);
+  public function __construct(ControllerResolverInterface $controller_resolver, TitleResolverInterface $title_resolver, UrlGeneratorInterface $url_generator) {
+    parent::__construct($title_resolver, $url_generator);
 
     $this->controllerResolver = $controller_resolver;
   }
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
index 36f2891..6a854d6 100644
--- a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
@@ -88,6 +88,11 @@ public function onHtmlPage(GetResponseForControllerResultEvent $event) {
       if ($max_age = $page->getCacheMaxAge()) {
         $response->headers->set('cache_max_age', $max_age);
       }
+
+      // Set the generator in the HTTP header.
+      list($version) = explode('.', \Drupal::VERSION, 2);
+      $response->headers->set('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)');
+
       $event->setResponse($response);
     }
   }
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
index 8de2067..2a330f8 100644
--- a/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
+++ b/core/lib/Drupal/Core/Page/DefaultHtmlFragmentRenderer.php
@@ -85,7 +85,36 @@ public function preparePage(HtmlPage $page, &$page_array) {
     $html_attributes['lang'] = $language_interface->id;
     $html_attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
 
+    $this->setDefaultMetaTags($page);
+
     return $page;
   }
 
+  /**
+   * Apply the default meta tags to the page object.
+   *
+   * @param \Drupal\Core\Page\HtmlPage $page
+   *   The html page.
+   */
+  protected function setDefaultMetaTags(HtmlPage $page) {
+    // Add default elements. 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.
+    $page->addMetaElement(new MetaElement(NULL, array(
+      'name' => 'charset',
+      'charset' => 'utf-8',
+    )));
+    // Show Drupal and the major version number in the META GENERATOR tag.
+    // Get the major version.
+    list($version) = explode('.', \Drupal::VERSION, 2);
+    $page->addMetaElement(new MetaElement('Drupal ' . $version . ' (http://drupal.org)', array(
+      'name' => 'Generator',
+    )));
+
+    // Display the html.html.twig's default mobile metatags for responsive design.
+    $page->addMetaElement(new MetaElement(NULL, array('name' => 'MobileOptimized', 'content' => 'width')));
+    $page->addMetaElement(new MetaElement(NULL, array('name' => 'HandheldFriendly', 'content' => 'true')));
+    $page->addMetaElement(new MetaElement(NULL, array('name' => 'viewport', 'content' => 'width=device-width')));
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Page/FeedLinkElement.php b/core/lib/Drupal/Core/Page/FeedLinkElement.php
new file mode 100644
index 0000000..648963b
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/FeedLinkElement.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\FeedLinkElement.
+ */
+
+namespace Drupal\Core\Page;
+
+/**
+ * Defines a link to a feed.
+ */
+class FeedLinkElement extends LinkElement {
+
+  /**
+   * Creates a FeedLink instance.
+   *
+   * @param string $title
+   *   The title of the feed.
+   * @param string $href
+   *   The absolute URL to the feed.
+   */
+  public function __construct($title, $href) {
+    $rel = 'alternate';
+    $attributes['title'] = $title;
+    $attributes['type'] = 'application/rss+xml';
+
+    parent::__construct($href, $rel, $attributes);
+  }
+
+}
+
diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php
index 2c92965..85055ad 100644
--- a/core/lib/Drupal/Core/Page/HeadElement.php
+++ b/core/lib/Drupal/Core/Page/HeadElement.php
@@ -72,6 +72,16 @@ public function setAttribute($key, $value) {
   }
 
   /**
+   * Gets all the attributes.
+   *
+   * @return array
+   *   An array of all the attributes keyed by name of attribute.
+   */
+  public function &getAttributes() {
+    return $this->attributes;
+  }
+
+  /**
    * Sets if this element should be wrapped in <noscript>.
    *
    * @param bool $value
diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php
index cf1ca4d..c83aab0 100644
--- a/core/lib/Drupal/Core/Page/HtmlFragment.php
+++ b/core/lib/Drupal/Core/Page/HtmlFragment.php
@@ -22,6 +22,20 @@
 class HtmlFragment implements CacheableInterface {
 
   /**
+   * An array of Link elements.
+   *
+   * @var array
+   */
+  protected $links = array();
+
+  /**
+   * An array of Meta elements.
+   *
+   * @var array
+   */
+  protected $metatags = array();
+
+  /**
    * HTML content string.
    *
    * @var string
@@ -62,6 +76,50 @@ public function __construct($content = '', array $cache_info = array()) {
   }
 
   /**
+   * Adds a link element to the page.
+   *
+   * @param \Drupal\Core\Page\LinkElement $link
+   *   A link element to enqueue.
+   *
+   * @return $this
+   */
+  public function addLinkElement(LinkElement $link) {
+    $this->links[] = $link;
+    return $this;
+  }
+
+  /**
+   * Returns an array of all enqueued links.
+   *
+   * @return \Drupal\Core\Page\LinkElement[]
+   */
+  public function &getLinkElements() {
+    return $this->links;
+  }
+
+  /**
+   * Adds a meta element to the page.
+   *
+   * @param \Drupal\Core\Page\MetaElement $meta
+   *   A meta element to add.
+   *
+   * @return $this
+   */
+  public function addMetaElement(MetaElement $meta) {
+    $this->metatags[] = $meta;
+    return $this;
+  }
+
+  /**
+   * Returns an array of all enqueued meta elements.
+   *
+   * @return \Drupal\Core\Page\MetaElement[]
+   */
+  public function &getMetaElements() {
+    return $this->metatags;
+  }
+
+  /**
    * Sets the response content.
    *
    * This should be the bulk of the page content, and will ultimately be placed
@@ -73,8 +131,7 @@ public function __construct($content = '', array $cache_info = array()) {
    * @param mixed $content
    *   The content for this fragment.
    *
-   * @return self
-   *   The fragment.
+   * @return $this
    */
   public function setContent($content) {
     $this->content = $content;
@@ -108,6 +165,8 @@ public function getContent() {
    *   \Drupal\Component\Utility\String::checkPlain() or
    *   \Drupal\Component\Utility\Xss::filterAdmin(). With this flag the string
    *   will be passed through unchanged.
+   *
+   * @return $this
    */
   public function setTitle($title, $output = Title::CHECK_PLAIN) {
     if ($output == Title::CHECK_PLAIN) {
@@ -119,6 +178,7 @@ public function setTitle($title, $output = Title::CHECK_PLAIN) {
     else {
       $this->title = $title;
     }
+    return $this;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php
index f8725c4..300eee3 100644
--- a/core/lib/Drupal/Core/Page/HtmlPage.php
+++ b/core/lib/Drupal/Core/Page/HtmlPage.php
@@ -92,7 +92,7 @@ public function getBodyAttributes() {
    * @param string $content
    *   The top-content to set.
    *
-   * @return self
+   * @return $this
    *   The called object.
    */
   public function setBodyTop($content) {
@@ -116,7 +116,7 @@ public function getBodyTop() {
    * @param string $content
    *   The bottom-content to set.
    *
-   * @return self
+   * @return $this
    *   The called object.
    */
   public function setBodyBottom($content) {
@@ -140,7 +140,7 @@ public function getBodyBottom() {
    * @param int $status
    *   The status code to set.
    *
-   * @return self
+   * @return $this
    *   The called object.
    */
   public function setStatusCode($status) {
@@ -163,9 +163,29 @@ public function getStatusCode() {
    *
    * @param array $cache_tags
    *   The cache tags associated with this HTML page.
+   *
+   * @return $this
+   *   The called object.
    */
   public function setCacheTags(array $cache_tags) {
     $this->cache['tags'] = $cache_tags;
+    return $this;
+  }
+
+  /**
+   * Gets all feed links.
+   *
+   * @return \Drupal\Core\Page\FeedLinkElement[]
+   *   A list of feed links attached to the page.
+   */
+  public function getFeedLinks() {
+    $feed_links = array();
+    foreach ($this->getLinkElements() as $link) {
+      if ($link instanceof FeedLinkElement) {
+        $feed_links[] = $link;
+      }
+    }
+    return $feed_links;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Page/LinkElement.php b/core/lib/Drupal/Core/Page/LinkElement.php
new file mode 100644
index 0000000..04cb2a0
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/LinkElement.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\LinkElement.
+ */
+
+namespace Drupal\Core\Page;
+
+/**
+ * Defines a link html HEAD element, which is defined by the href of the link.
+ */
+class LinkElement extends HeadElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  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. Defaults to an empty string
+   * @param array $attributes
+   *   (optional) Additional attributes for this element. Defaults to an empty
+   *   array.
+   */
+  public function __construct($href, $rel = '', array $attributes = array()) {
+    $this->attributes = $attributes + array(
+      'href' => $href,
+      'rel' => $rel,
+    );
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Page/MetaElement.php b/core/lib/Drupal/Core/Page/MetaElement.php
new file mode 100644
index 0000000..d849ed9
--- /dev/null
+++ b/core/lib/Drupal/Core/Page/MetaElement.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Page\MetaElement.
+ */
+
+namespace Drupal\Core\Page;
+
+/**
+ * Defines a metatag HTML head element which is defined by the name and content.
+ */
+class MetaElement extends HeadElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $element = 'meta';
+
+  /**
+   * Constructs a new MetaElement instance.
+   *
+   * @param string $content
+   *   (optional) The value of the attribute, defaults to an empty string.
+   * @param array $attributes
+   *   (optional) Additional attributes for this element, defaults to an empty
+   *   array.
+   */
+  public function __construct($content = '', array $attributes = array()) {
+    $this->attributes = $attributes + array(
+      'content' => $content,
+    );
+  }
+
+  /**
+   * Sets the name attribute.
+   *
+   * @param string $name
+   *   The name attribute value to set.
+   *
+   * @return $this
+   */
+  public function setName($name) {
+    $this->attributes['name'] = $name;
+    return $this;
+  }
+
+  /**
+   * Sets the content attribute.
+   *
+   * @param string $content
+   *   The content attribute value to set.
+   *
+   * @return $this
+   */
+  public function setContent($content) {
+    $this->attributes['content'] = $content;
+    return $this;
+  }
+
+  /**
+   * Gets the name.
+   *
+   * @return string
+   *   The name of the metatag element.
+   */
+  public function getName() {
+    return $this->attributes['name'];
+  }
+
+  /**
+   * Gets the content.
+   *
+   * @return string
+   *   The content of the metatag.
+   */
+  public function getContent() {
+    return $this->attributes['content'];
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/AddFeedTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/AddFeedTest.php
index b871710..2f4689b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/AddFeedTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/AddFeedTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\system\Tests\Common;
 
+use Drupal\Core\Page\FeedLinkElement;
+use Drupal\Core\Page\HtmlPage;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -39,46 +41,41 @@ function testBasicFeedAddNoTitle() {
     // - 'title' == the title of the feed as passed into drupal_add_feed().
     $urls = array(
       'path without title' => array(
-        'input_url' => $path,
-        'output_url' => url($path, array('absolute' => TRUE)),
+        'url' => url($path, array('absolute' => TRUE)),
         'title' => '',
       ),
       'external URL without title' => array(
-        'input_url' => $external_url,
-        'output_url' => $external_url,
+        'url' => $external_url,
         'title' => '',
       ),
       'local URL without title' => array(
-        'input_url' => $fully_qualified_local_url,
-        'output_url' => $fully_qualified_local_url,
+        'url' => $fully_qualified_local_url,
         'title' => '',
       ),
       'path with title' => array(
-        'input_url' => $path_for_title,
-        'output_url' => url($path_for_title, array('absolute' => TRUE)),
+        'url' => url($path_for_title, array('absolute' => TRUE)),
         'title' => $this->randomName(12),
       ),
       'external URL with title' => array(
-        'input_url' => $external_for_title,
-        'output_url' => $external_for_title,
+        'url' => $external_for_title,
         'title' => $this->randomName(12),
       ),
       'local URL with title' => array(
-        'input_url' => $fully_qualified_for_title,
-        'output_url' => $fully_qualified_for_title,
+        'url' => $fully_qualified_for_title,
         'title' => $this->randomName(12),
       ),
     );
 
+    $html_page = new HtmlPage();
+
     foreach ($urls as $description => $feed_info) {
-      $build['#attached']['drupal_add_feed'][] = array($feed_info['input_url'], $feed_info['title']);
+      $feed_link = new FeedLinkElement($feed_info['title'], $feed_info['url']);
+      $html_page->addLinkElement($feed_link);
     }
 
-    drupal_render($build);
-
-    $this->drupalSetContent(drupal_get_html_head());
+    $this->drupalSetContent(\Drupal::service('html_page_renderer')->render($html_page));
     foreach ($urls as $description => $feed_info) {
-      $this->assertPattern($this->urlToRSSLinkPattern($feed_info['output_url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
+      $this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
     }
   }
 
@@ -88,7 +85,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 +rel="alternate" +type="application/rss.xml" +title="' . $title . '" +href="' . $url . '" */>%';
+    $generated_pattern = '%<link +title="' . $title . '" +type="application/rss.xml" +href="' . $url . '" +rel="alternate" */>%';
     return $generated_pattern;
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Page/DefaultMetatagsTest.php b/core/modules/system/lib/Drupal/system/Tests/Page/DefaultMetatagsTest.php
new file mode 100644
index 0000000..baae991
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Page/DefaultMetatagsTest.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\simpletest\Tests\Page\DefaultMetatagsTest.
+ */
+
+namespace Drupal\system\Tests\Page;
+
+use Drupal\simpletest\WebTestBase;
+
+class DefaultMetatagsTest extends WebTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Default HTML metatags',
+      'description' => 'Tests the default HTML metatags on the page.',
+      'group' => 'Page',
+    );
+  }
+
+  /**
+   * Tests meta tags.
+   */
+  public function testMetaTag() {
+    $this->drupalGet('');
+    // Ensures that the charset metatag is on the page.
+    $result = $this->xpath('//meta[@name="charset" and @charset="utf-8"]');
+    $this->assertEqual(count($result), 1);
+
+    // Ensure that the charset one is the first metatag.
+    $result = $this->xpath('//meta');
+    $this->assertEqual((string) $result[0]->attributes()->name, 'charset');
+    $this->assertEqual((string) $result[0]->attributes()->charset, 'utf-8');
+  }
+
+}
+
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 4447052..c69838a 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -2310,27 +2310,6 @@ function hook_install_tasks(&$install_state) {
 }
 
 /**
- * Alter XHTML HEAD tags before they are rendered by drupal_get_html_head().
- *
- * Elements available to be altered are only those added using
- * drupal_add_html_head_link() or drupal_add_html_head(). CSS and JS files
- * are handled using _drupal_add_css() and _drupal_add_js(), so the head links
- * for those files will not appear in the $head_elements array.
- *
- * @param $head_elements
- *   An array of renderable elements. Generally the values of the #attributes
- *   array will be the most likely target for changes.
- */
-function hook_html_head_alter(&$head_elements) {
-  foreach ($head_elements as $key => $element) {
-    if (isset($element['#attributes']['rel']) && $element['#attributes']['rel'] == 'canonical') {
-      // I want a custom canonical URL.
-      $head_elements[$key]['#attributes']['href'] = mymodule_canonical_url();
-    }
-  }
-}
-
-/**
  * Alter the full list of installation tasks.
  *
  * You can use this hook to change or replace any part of the Drupal
diff --git a/core/modules/system/tests/modules/system_module_test/lib/Drupal/system_module_test/EventSubscriber/HtmlPageSubscriber.php b/core/modules/system/tests/modules/system_module_test/lib/Drupal/system_module_test/EventSubscriber/HtmlPageSubscriber.php
new file mode 100644
index 0000000..f64cd54
--- /dev/null
+++ b/core/modules/system/tests/modules/system_module_test/lib/Drupal/system_module_test/EventSubscriber/HtmlPageSubscriber.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system_module_test\EventSubscriber\HtmlPageSubscriber.
+ */
+
+namespace Drupal\system_module_test\EventSubscriber;
+use Drupal\Core\Page\HtmlPage;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Defines an event subscriber to alter some metatags.
+ */
+class HtmlPageSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Adds some metatags to the HTML page object.
+   */
+  public function onHtmlPage(GetResponseForControllerResultEvent $event) {
+    if (($page = $event->getControllerResult()) && $page instanceof HtmlPage) {
+      $metatags =& $page->getMetaElements();
+      foreach ($metatags as $key => $tag) {
+        // Remove the HTML5 mobile meta-tags.
+        if (in_array($tag->getName(), array('MobileOptimized', 'HandheldFriendly', 'viewport', 'cleartype'))) {
+          unset($metatags[$key]);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Execute between
+    // \Drupal\Core\EventSubscriber\HtmlViewSubscriber::onHtmlFragment and
+    // \Drupal\Core\EventSubscriber\HtmlViewSubscriber::onHtmlPage and
+    $events[KernelEvents::VIEW][] = array('onHtmlPage', 60);
+    return $events;
+  }
+
+}
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 44dd782..8d67e7a 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,13 +5,3 @@
  * Provides System module hook implementations for testing purposes.
  */
 
-/**
- * Implements hook_html_head_alter().
- */
-function system_module_test_html_head_alter(&$head_elements) {
-  // Remove the HTML5 mobile meta-tags.
-  unset($head_elements['MobileOptimized']);
-  unset($head_elements['HandheldFriendly']);
-  unset($head_elements['viewport']);
-  unset($head_elements['cleartype']);
-}
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
new file mode 100644
index 0000000..3abeb5e
--- /dev/null
+++ b/core/modules/system/tests/modules/system_module_test/system_module_test.services.yml
@@ -0,0 +1,5 @@
+services:
+  system_module_test.html_page_subscriber:
+    class: Drupal\system_module_test\EventSubscriber\HtmlPageSubscriber
+    tags:
+      - { name: event_subscriber }
diff --git a/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php b/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php
index 34f2af9..50d0172 100644
--- a/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php
+++ b/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php
@@ -39,13 +39,14 @@ public function test405HTML() {
     $html_fragment_renderer = $this->getMock('Drupal\Core\Page\HtmlFragmentRendererInterface');
     $title_resolver = $this->getMock('Drupal\Core\Controller\TitleResolverInterface');
     $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface');
+    $url_generator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
 
     $content_negotiation = $this->getMock('Drupal\Core\ContentNegotiation');
     $content_negotiation->expects($this->any())
       ->method('getContentType')
       ->will($this->returnValue('html'));
 
-    $exception_controller = new ExceptionController($content_negotiation, $title_resolver, $html_page_renderer, $html_fragment_renderer, $translation);
+    $exception_controller = new ExceptionController($content_negotiation, $title_resolver, $html_page_renderer, $html_fragment_renderer, $translation, $url_generator);
     $response = $exception_controller->execute($flat_exception, new Request());
     $this->assertEquals($response->getStatusCode(), 405, 'HTTP status of response is correct.');
     $this->assertEquals($response->getContent(), 'Method Not Allowed', 'HTTP response body is correct.');
diff --git a/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php b/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php
new file mode 100644
index 0000000..ed73bad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Page/HtmlPageTest.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Page\HtmlPageTest.
+ */
+
+namespace Drupal\Tests\Core\Page;
+
+use Drupal\Core\Page\HtmlPage;
+use Drupal\Core\Page\MetaElement;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the HTML page object.
+ *
+ * @coversDefaultClass \Drupal\Core\Page\HtmlPage
+ */
+class HtmlPageTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests \Drupal\Core\Page\HtmlPage',
+      'description' => '',
+      'group' => 'Page',
+    );
+  }
+
+  /**
+   * Ensures that a single metatags can be changed.
+   */
+  public function testMetatagAlterability() {
+    $page = new HtmlPage();
+    $page->addMetaElement(new MetaElement('', array('name' => 'example')));
+    $page->addMetaElement(new MetaElement('', array('name' => 'example2')));
+    $metatags = $page->getMetaElements();
+
+    foreach ($metatags as $tag) {
+      if ($tag->getName() == 'example') {
+        $tag->setContent('hello');
+      }
+    }
+
+    $metatags = $page->getMetaElements();
+    $this->assertEquals('hello', $metatags[0]->getContent());
+    $this->assertEquals('', $metatags[1]->getContent());
+  }
+
+  /**
+   * Ensures that a metatag can be removed.
+   */
+  public function testMetatagRemovability() {
+    $page = new HtmlPage();
+    $page->addMetaElement(new MetaElement('', array('name' => 'example')));
+    $page->addMetaElement(new MetaElement('', array('name' => 'example2')));
+    $metatags =& $page->getMetaElements();
+
+    foreach ($metatags as $key => $tag) {
+      if ($tag->getName() == 'example') {
+        unset($metatags[$key]);
+      }
+    }
+
+    $metatags = $page->getMetaElements();
+    reset($metatags);
+    $this->assertCount(1, $metatags);
+    $this->assertEquals('example2', current($metatags)->getName());
+  }
+
+}
+
