diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
index 30c8252..beaa4e3 100644
--- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
@@ -253,6 +253,41 @@ class EntityType extends Plugin {
   public $menu_path_wildcard;
 
   /**
+   * Link templates using the URI template syntax.
+   *
+   * Links are an array of standard link relations to the URI template that
+   * should be used for them. Where possible, link relationships should use
+   * established IANA relationships rather than custom relationships.
+   *
+   * Every entity type should, at minimum, define "canonical", which is the
+   * pattern for URIs to that entity. Even if the entity will have no HTML page
+   * exposed to users it should still have a canonical URI in order to be
+   * compatible with web services. Entities that will be user-editable via an
+   * HTML page must also define an "edit-form" relationship.
+   *
+   * By default, the following placeholders are supported:
+   * - entityType: The machine name of the entity type.
+   * - bundle: The bundle machine name of the entity.
+   * - id: The unique ID of the entity.
+   * - uuid: The UUID of the entity.
+   * - [entityType]: The entity type itself will also be a valid token for the
+   *   ID of the entity. For instance, a placeholder of {node} used on the Node
+   *   class would have the same value as {id}. This is generally preferred
+   *   over "id" for better self-documentation.
+   *
+   * Specific entity types may also expand upon this list by overriding the
+   * uriPlaceholderReplacements() method.
+   *
+   * @link http://www.iana.org/assignments/link-relations/link-relations.xml @endlink
+   * @link http://tools.ietf.org/html/rfc6570 @endlink
+   *
+   * @var array
+   */
+  public $links = array(
+    'canonical' => '/entity/{entityType}/{id}',
+  );
+
+  /**
    * Specifies whether a module exposing permissions for the current entity type
    * should use entity-type level granularity, bundle level granularity or just
    * skip this entity. The allowed values are respectively "entity_type",
diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
index b27dae6..a9eadea 100644
--- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
+++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
@@ -214,6 +214,13 @@ function __clone() {
   /**
    * Forwards the call to the decorated entity.
    */
+  public function uriRelationships() {
+    return $this->decorated->uriRelationships();
+  }
+
+  /**
+   * Forwards the call to the decorated entity.
+   */
   public function access($operation = 'view', UserInterface $account = NULL) {
     return $this->decorated->access($operation, $account);
   }
@@ -355,8 +362,8 @@ public function label($langcode = NULL) {
   /**
    * Forwards the call to the decorated entity.
    */
-  public function uri() {
-    return $this->decorated->uri();
+  public function uri($rel = 'canonical') {
+    return $this->decorated->uri($rel);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 078094c..3b18899 100644
--- a/core/lib/Drupal/Core/Entity/EntityNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityNG.php
@@ -74,6 +74,13 @@ class EntityNG extends Entity {
   protected $fieldDefinitions;
 
   /**
+   * Local cache for URI placeholder substitution values.
+   *
+   * @var array
+   */
+  protected $uriPlaceholderReplacements;
+
+  /**
    * Overrides Entity::__construct().
    */
   public function __construct(array $values, $entity_type, $bundle = FALSE) {
@@ -136,6 +143,72 @@ public function uuid() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function uri($rel = 'canonical') {
+    $entity_info = $this->entityInfo();
+
+    $link_templates = isset($entity_info['links']) ? $entity_info['links'] : array();
+
+    if (isset($link_templates[$rel])) {
+      $template = $link_templates[$rel];
+      $replacements = $this->uriPlaceholderReplacements();
+      $uri['path'] = str_replace(array_keys($replacements), array_values($replacements), $template);
+
+      // @todo Remove this once http://drupal.org/node/1888424 is in and we can
+      //   move the BC handling of / vs. no-/ to the generator.
+      $uri['path'] = trim($uri['path'], '/');
+
+      // Pass the entity data to url() so that alter functions do not need to
+      // look up this entity again.
+      $uri['options']['entity_type'] = $this->entityType;
+      $uri['options']['entity'] = $this;
+      return $uri;
+    }
+
+    // For a canonical link (that is, a link to self), look up the stack for
+    // default logic. Other relationship types are not supported by parent
+    // classes.
+    if ($rel == 'canonical') {
+      return parent::uri();
+    }
+  }
+
+  /**
+   * Returns a list of URI relationships supported by this entity.
+   *
+   * @return array
+   *   An array of link relationships supported by this entity.
+   */
+  public function uriRelationships() {
+    $entity_info = $this->entityInfo();
+    return isset($entity_info['links']) ? array_keys($entity_info['links']) : array();
+  }
+
+  /**
+   * Returns an array of placeholders for this entity.
+   *
+   * Individual entity classes may override this method to add additional
+   * placeholders if desired. If so, they should be sure to replicate the
+   * property caching logic.
+   *
+   * @return array
+   *   An array of URI placeholders.
+   */
+  protected function uriPlaceholderReplacements() {
+    if (empty($this->uriPlaceholderReplacements)) {
+      $this->uriPlaceholderReplacements = array(
+        '{entityType}' => $this->entityType(),
+        '{bundle}' => $this->bundle(),
+        '{id}' => $this->id(),
+        '{uuid}' => $this->uuid(),
+        '{' . $this->entityType() . '}' => $this->id(),
+      );
+    }
+    return $this->uriPlaceholderReplacements;
+  }
+
+  /**
    * Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
    */
   public function get($property_name) {
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
index 55a1bd3..e774c3e 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
@@ -41,6 +41,10 @@
  *     "bundle" = "node_type",
  *     "label" = "subject",
  *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" = "/comment/{comment}",
+ *     "edit-form" = "/comment/{comment}/edit"
  *   }
  * )
  */
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index fc4da34..00c132c 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -46,7 +46,12 @@
  *     "bundle" = "type"
  *   },
  *   route_base_path = "admin/structure/types/manage/{bundle}",
- *   permission_granularity = "bundle"
+ *   permission_granularity = "bundle",
+ *   links = {
+ *     "canonical" = "/node/{node}",
+ *     "edit-form" = "/node/{node}/edit",
+ *     "version-history" = "/node/{node}/revisions"
+ *   }
  * )
  */
 class Node extends EntityNG implements NodeInterface {
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 30161a2..3799df1 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -2137,11 +2137,18 @@ function node_page_view(EntityInterface $node) {
   // of the active trail, and the link name becomes the page title.
   // Thus, we must explicitly set the page title to be the node title.
   drupal_set_title($node->label());
-  $uri = $node->uri();
-  // Set the node path as the canonical URL to prevent duplicate content.
-  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
-  // Set the non-aliased path as a default shortlink.
-  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
+
+  foreach ($node->uriRelationships() as $rel) {
+    $uri = $node->uri($rel);
+    // Set the node path as the canonical URL to prevent duplicate content.
+    drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE);
+
+    if ($rel == 'canonical') {
+      // Set the non-aliased canonical path as a default shortlink.
+      drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
+    }
+  }
+
   return node_show($node);
 }
 
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
index 28741ec..5f3dc78 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
@@ -43,6 +43,10 @@
  *   bundle_keys = {
  *     "bundle" = "vid"
  *   },
+ *   links = {
+ *     "canonical" = "/taxonomy/term/{taxonomy_term}",
+ *     "edit-form" = "/taxonomy/term/{taxonomy_term}/edit"
+ *   },
  *   menu_base_path = "taxonomy/term/%taxonomy_term",
  *   route_base_path = "admin/structure/taxonomy/manage/{bundle}",
  *   permission_granularity = "bundle"
diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc
index 72c1fb1..4865b3e 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -32,12 +32,16 @@ function taxonomy_term_page(Term $term) {
   drupal_set_breadcrumb($breadcrumb);
   drupal_add_feed('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->label());
 
-  $uri = $term->uri();
-
-  // Set the term path as the canonical URL to prevent duplicate content.
-  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
-  // Set the non-aliased path as a default shortlink.
-  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
+  foreach ($term->uriRelationships() as $rel) {
+    $uri = $term->uri($rel);
+    // Set the term path as the canonical URL to prevent duplicate content.
+    drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE);
+
+    if ($rel == 'canonical') {
+      // Set the non-aliased canonical path as a default shortlink.
+      drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
+    }
+  }
 
   $build['taxonomy_terms'] = taxonomy_term_view_multiple(array($term->id() => $term));
   if ($nids = taxonomy_select_nodes($term->id(), TRUE, config('node.settings')->get('items_per_page'))) {
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
index 6c95606..bca2734 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
@@ -41,6 +41,10 @@
  *   entity_keys = {
  *     "id" = "uid",
  *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" = "/user/{user}",
+ *     "edit-form" = "/user/{user}/edit"
  *   }
  * )
  */
