diff --git CHANGELOG.txt CHANGELOG.txt
index 5b02767..e559008 100644
--- CHANGELOG.txt
+++ CHANGELOG.txt
@@ -142,6 +142,13 @@ Drupal 7.0, xxxx-xx-xx (development version)
 - Added RDF support:
     * Modules can declare RDF namespaces which are serialized in the <html> tag
       for RDFa support.
+- Search engine optimization and web linking:
+     * Added a rel="canonical" link on node and comment pages to prevent
+       duplicate content indexing by search engines.
+     * Added default a rel="shortlink" link on node and comment pages
+       that advertises a short link as an alternative to third-party services.
+     * drupal_set_html_head now stores structured data for link, meta, and
+       other elements, allowing it to be altered by modules before rendering.
 - Field API:
     * Custom data fields may be attached to nodes, users, comments and taxonomy
       terms.
diff --git includes/batch.inc includes/batch.inc
index 289626e..2e137aa 100644
--- includes/batch.inc
+++ includes/batch.inc
@@ -190,7 +190,14 @@ function _batch_progress_page_nojs() {
   }
 
   $url = url($batch['url'], array('query' => array('id' => $batch['id'], 'op' => $new_op)));
-  drupal_add_html_head('<meta http-equiv="Refresh" content="0; URL=' . $url . '">');
+  $element = array(
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'http-equiv' => 'Refresh',
+      'content' => '0; URL=' . $url,
+    ),
+  );
+  drupal_add_html_head($element, 'batch_progress_meta_refresh');
 
   return theme('progress_bar', $percentage, $message);
 }
diff --git includes/common.inc includes/common.inc
index 7d646e1..f415d69 100644
--- includes/common.inc
+++ includes/common.inc
@@ -250,22 +250,71 @@ function drupal_get_rdf_namespaces() {
  * Add output to the head tag of the HTML page.
  *
  * This function can be called as long the headers aren't sent.
+ *
+ * @param $data
+ *   A renderable array. If the '#type' key is not set then 'xhtml_tag' will be
+ *   added as the default '#type'.
+ * @param $key
+ *   A unique string key identifying the data.  Required if $data is not NULL.
+ *
+ * @return
+ *   An array of all the stored HEAD elements.
  */
-function drupal_add_html_head($data = NULL) {
-  $stored_head = &drupal_static(__FUNCTION__, '');
+function drupal_add_html_head($data = NULL, $key = NULL) {
+  $stored_head = &drupal_static(__FUNCTION__);
 
-  if (!is_null($data)) {
-    $stored_head .= $data . "\n";
+  if (!isset($stored_head)) {
+    // Make sure the defaults, including Content-Type, come first.
+    $stored_head = _drupal_default_html_head();
+  }
+
+  if (isset($data) && isset($key)) {
+    if (!isset($data['#type'])) {
+      $data['#type'] = 'xhtml_tag';
+    }
+    $stored_head[$key] = $data;
   }
   return $stored_head;
 }
 
 /**
- * Retrieve output to be displayed in the head tag of the HTML page.
+ * Returns elements 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' => 'xhtml_tag',
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'http-equiv' => 'Content-Type',
+      'content' => 'text/html; charset=utf-8',
+    ),
+  );
+  // Get the major version.
+  list($version, ) = explode('.', VERSION);
+  // Show Drupal and the major version number in the META GENERATOR tag.
+  $elements['system_meta_generator'] = array(
+    '#type' => 'xhtml_tag',
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'name' => 'Generator',
+      'content' => 'Drupal ' . $version . ' (http://drupal.org)',
+    ),
+  );
+  // Send Drupal and the major version number in the HTTP headers.
+  $elements['system_meta_generator']['#attached']['drupal_set_header'][] = array('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)');
+  return $elements;
+}
+
+/**
+ * Retrieve output to be displayed in the HEAD tag of the HTML page.
  */
 function drupal_get_html_head() {
-  $output = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
-  return $output . drupal_add_html_head();
+  $head_elements = drupal_add_html_head();
+  drupal_alter('html_head', $head_elements);
+  return drupal_render($head_elements);
 }
 
 /**
@@ -288,13 +337,19 @@ function drupal_clear_path_cache() {
 function drupal_add_feed($url = NULL, $title = '') {
   $stored_feed_links = &drupal_static(__FUNCTION__, array());
 
-  if (!is_null($url) && !isset($stored_feed_links[$url])) {
+  if (isset($url)) {
     $stored_feed_links[$url] = theme('feed_icon', $url, $title);
 
-    drupal_add_link(array('rel' => 'alternate',
-                          'type' => 'application/rss+xml',
-                          'title' => $title,
-                          'href' => $url));
+    $element = array(
+      '#tag' => 'link',
+      '#attributes' => array(
+        'rel' => 'alternate',
+        'type' => 'application/rss+xml',
+        'title' => $title,
+        'href' => $url,
+      ),
+    );
+    drupal_add_html_head($element, 'drupal_add_feed-' . $url);
   }
   return $stored_feed_links;
 }
@@ -2295,6 +2350,28 @@ function url($path = NULL, array $options = array()) {
 }
 
 /**
+ * Format an attribute string for a HTTP header.
+ *
+ * @param $attributes
+ *   An associative array of attributes.
+ *
+ * @return
+ *   A ; separated string ready for insertion in a HTTP header. No escaping is
+ *   performed for HTML entities, so this string is not safe to be printed.
+ *
+ * @see drupal_set_header()
+ */
+function drupal_header_attributes(array $attributes = array()) {
+  foreach ($attributes as $attribute => &$data) {
+    if (is_array($data)) {
+      $data = implode(' ', $data);
+    }
+    $data = $attribute . '="' . $data . '"';
+  }
+  return $attributes ? ' ' . implode('; ', $attributes) : '';
+}
+
+/**
  * Format an attribute string to insert in a tag.
  *
  * Each array key and its value will be formatted into an HTML attribute string.
@@ -2503,12 +2580,32 @@ function base_path() {
 }
 
 /**
- * Add a <link> tag to the page's HEAD.
+ * Add a LINK tag with a distinct REL attribute to the page's HEAD.
  *
- * This function can be called as long the HTML header hasn't been sent.
- */
-function drupal_add_link($attributes) {
-  drupal_add_html_head('<link' . drupal_attributes($attributes) . " />\n");
+ * This function can be called as long the HTML header hasn't been sent,
+ * which on normal pages is up through the preprocess step of theme('html').
+ * Adding a link will overwrite a priot link with the same REL attribute.
+ * Use drupal_add_html_head(), drupal_add_css(), or drupal_add_js() to add
+ * multiple links with the same REL.
+ * 
+ * @param $attributes
+ *   Associative array of element attributes including 'href' and 'rel'.
+ * @param $header
+ *   Optional flag to determine if a HTTP Link: header should be sent.
+ */
+function drupal_add_link($attributes, $header = FALSE) {
+  $element = array(
+    '#tag' => 'link',
+    '#attributes' => $attributes,
+  );
+
+  if ($header) {
+    // Set a HTTP Link: header also.
+    $href = '<' . check_plain($attributes['href']) . '>;';
+    unset($attributes['href']);
+    $element['#attached']['drupal_set_header'][] = array('Link',  $href . drupal_header_attributes($attributes), TRUE);
+  }
+  drupal_add_html_head($element, 'drupal_add_link-' . $attributes['rel']);
 }
 
 /**
@@ -4610,6 +4707,9 @@ function drupal_common_theme() {
     'indentation' => array(
       'arguments' => array('size' => 1),
     ),
+    'xhtml_tag' => array(
+      'arguments' => array('element' => NULL),
+    ),
     // from pager.inc
     'pager' => array(
       'arguments' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9),
diff --git includes/theme.inc includes/theme.inc
index 34e4787..e75d923 100644
--- includes/theme.inc
+++ includes/theme.inc
@@ -1849,6 +1849,20 @@ function theme_feed_icon($url, $title) {
 }
 
 /**
+ * Generate the output for a generic XHTML tag with attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_xhtml_tag(array $element) {
+  if (!isset($element['#value'])) {
+    return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . " />\n";
+  }
+  else {
+    return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>' . $element['#value'] . '</' . $element['#tag'] . ">\n";
+  }
+}
+
+/**
  * Returns code that emits the 'more' link used on blocks.
  *
  * @param $url
@@ -2140,7 +2154,7 @@ function template_preprocess_html(&$variables) {
   if (theme_get_setting('toggle_favicon')) {
     $favicon = theme_get_setting('favicon');
     $type = theme_get_setting('favicon_mimetype');
-    drupal_add_html_head('<link rel="shortcut icon" href="' . check_url($favicon) . '" type="' . check_plain($type) . '" />');
+    drupal_add_link(array('rel' => 'shortcut icon', 'href' => check_url($favicon), 'type' => $type));
   }
 
   // Construct page title.
@@ -2310,7 +2324,7 @@ function template_preprocess_maintenance_page(&$variables) {
   if (theme_get_setting('toggle_favicon')) {
     $favicon = theme_get_setting('favicon');
     $type = theme_get_setting('favicon_mimetype');
-    drupal_add_html_head('<link rel="shortcut icon" href="' . check_url($favicon) . '" type="' . check_plain($type) . '" />');
+    drupal_add_link(array('rel' => 'shortcut icon', 'href' => check_url($favicon), 'type' => $type));
   }
 
   global $theme;
diff --git modules/comment/comment.module modules/comment/comment.module
index 3828702..233b1fd 100644
--- modules/comment/comment.module
+++ modules/comment/comment.module
@@ -349,9 +349,6 @@ function comment_permalink($comment) {
     $_GET['q'] = 'node/' . $node->nid;
     $_GET['page'] = $page;
 
-    // Set the node path as the canonical URL to prevent duplicate content.
-    drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)));
-
     // Return the node view, this will show the correct comment in context.
     return menu_execute_active_handler('node/' . $node->nid);
   }
diff --git modules/node/node.module modules/node/node.module
index fd6c2ff..91b0052 100644
--- modules/node/node.module
+++ modules/node/node.module
@@ -1957,6 +1957,10 @@ function node_page_default() {
  */
 function node_page_view($node) {
   drupal_set_title($node->title);
+  // Set the node path as the canonical URL to prevent duplicate content.
+  drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)), TRUE);
+  // Set the non-aliased path as a default shortlink.
+  drupal_add_link(array('rel' => 'shortlink', 'href' => url('node/' . $node->nid, array('alias' => TRUE))), TRUE);
   return node_show($node);
 }
 
diff --git modules/openid/tests/openid_test.module modules/openid/tests/openid_test.module
index b101b50..8f7a85e 100644
--- modules/openid/tests/openid_test.module
+++ modules/openid/tests/openid_test.module
@@ -97,7 +97,14 @@ function openid_test_yadis_x_xrds_location() {
  * Menu callback; regular HTML page with <meta> element.
  */
 function openid_test_yadis_http_equiv() {
-  drupal_add_html_head('<meta http-equiv="X-XRDS-Location" content="' . url('openid-test/yadis/xrds', array('absolute' => TRUE)) . '" />');
+  $element = array(
+    '#tag' => 'meta',
+    '#attributes' => array(
+      'http-equiv' => 'X-XRDS-Location',
+      'content' => url('openid-test/yadis/xrds', array('absolute' => TRUE)),
+    ),
+  );
+  drupal_add_html_head($element, 'openid_test_yadis_http_equiv');
   return t('This page includes a &lt;meta equiv=...&gt; element containing the URL of an XRDS document.');
 }
 
@@ -105,7 +112,7 @@ function openid_test_yadis_http_equiv() {
  * Menu callback; regular HTML page with OpenID 1.0 <link> element.
  */
 function openid_test_html_openid1() {
-  drupal_add_html_head('<link rel="openid.server" href="' . url('openid-test/endpoint', array('absolute' => TRUE)) . '" />');
+  drupal_add_link(array('rel' => 'openid.server', 'href' => url('openid-test/endpoint', array('absolute' => TRUE))));
   return t('This page includes a &lt;link rel=...&gt; element containing the URL of an OpenID Provider Endpoint.');
 }
 
@@ -113,7 +120,7 @@ function openid_test_html_openid1() {
  * Menu callback; regular HTML page with OpenID 2.0 <link> element.
  */
 function openid_test_html_openid2() {
-  drupal_add_html_head('<link rel="openid2.provider" href="' . url('openid-test/endpoint', array('absolute' => TRUE)) . '" />');
+  drupal_add_link(array('rel' => 'openid2.provider', 'href' => url('openid-test/endpoint', array('absolute' => TRUE))));
   return t('This page includes a &lt;link rel=...&gt; element containing the URL of an OpenID Provider Endpoint.');
 }
 
diff --git modules/simpletest/tests/browser_test.module modules/simpletest/tests/browser_test.module
index c07a675..15f52b4 100644
--- modules/simpletest/tests/browser_test.module
+++ modules/simpletest/tests/browser_test.module
@@ -59,7 +59,14 @@ function browser_test_print_post_form_submit($form, &$form_state) {
 function browser_test_refresh_meta() {
   if (!isset($_GET['refresh'])) {
     $url = url('browser_test/refresh/meta', array('absolute' => TRUE, 'query' => 'refresh=true'));
-    drupal_add_html_head('<meta http-equiv="Refresh" content="0; URL=' . $url . '">');
+    $element = array(
+      '#tag' => 'meta',
+      '#attributes' => array(
+        'http-equiv' => 'Refresh',
+        'content' => '0; URL=' . $url,
+      )
+    );
+    drupal_add_html_head($element, 'browser_test_refresh_meta');
     return '';
   }
   echo 'Refresh successful';
diff --git modules/system/system.api.php modules/system/system.api.php
index ada380a..206b64b 100644
--- modules/system/system.api.php
+++ modules/system/system.api.php
@@ -2189,6 +2189,27 @@ function hook_drupal_goto_alter(array $args) {
 }
 
 /**
+ * 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_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 MIME type mappings used to determine MIME type from a file extension.
  *
  * This hook is run when file_mimetype_mapping() is called. It is used to
diff --git modules/system/system.module modules/system/system.module
index d653f7c..355ae3b 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -194,12 +194,6 @@ function system_theme() {
     'system_powered_by' => array(
       'arguments' => array('image_path' => NULL),
     ),
-    'meta_generator_html' => array(
-      'arguments' => array('version' => NULL),
-    ),
-    'meta_generator_header' => array(
-      'arguments' => array('version' => NULL),
-    ),
     'system_compact_link' => array(),
     'system_run_cron_image' => array(
       'arguments' => array('image_path' => NULL),
@@ -300,6 +294,12 @@ function system_element_info() {
     '#items' => array(),
   );
 
+  $types['xhtml_tag'] = array(
+    '#theme' => 'xhtml_tag',
+    '#attributes' => array(),
+    '#value' => NULL,
+  );
+
   // Input elements.
   $types['submit'] = array(
     '#input' => TRUE,
@@ -1459,22 +1459,6 @@ function system_init() {
 }
 
 /**
- * Implement MODULE_preprocess_HOOK().
- */
-function system_preprocess_page(&$variables) {
-  // Get the major version
-  list($version, ) = explode('.', VERSION);
-
-  // Emit the META tag in the HTML HEAD section
-  theme('meta_generator_html', $version);
-
-  // Emit the HTTP Header too
-  theme('meta_generator_header', $version);
-
-  $variables['head'] = drupal_get_html_head();
-}
-
-/**
  * Implement hook_user_form().
  */
 function system_user_form(&$edit, $account, $category) {
@@ -2687,25 +2671,6 @@ function theme_system_compact_link() {
   return $output;
 }
 
-
-/**
- * Send Drupal and the major version number in the META GENERATOR HTML.
- *
- * @ingroup themeable
- */
-function theme_meta_generator_html($version = VERSION) {
-  drupal_add_html_head('<meta name="Generator" content="Drupal ' . $version . ' (http://drupal.org)" />');
-}
-
-/**
- * Send Drupal and the major version number in the HTTP headers.
- *
- * @ingroup themeable
- */
-function theme_meta_generator_header($version = VERSION) {
-  drupal_set_header('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)');
-}
-
 /**
  * Implement hook_image_toolkits().
  */
