diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
index 30c8252..7768bc9 100644
--- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
@@ -253,6 +253,37 @@ 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.
+   *
+   * 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 ffb0e0c..6c8be21 100644
--- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
+++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
@@ -213,6 +213,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', \Drupal\user\Plugin\Core\Entity\User $account = NULL) {
     return $this->decorated->access($operation, $account);
   }
@@ -354,8 +361,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 6a93a59..ea2c262 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..a291947 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/{id}",
+ *     "edit-form" = "/comment/{id}/edit"
  *   }
  * )
  */
diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentThreadingTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentThreadingTest.php
index 3636f91..9532099 100644
--- a/core/modules/comment/lib/Drupal/comment/Tests/CommentThreadingTest.php
+++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentThreadingTest.php
@@ -132,10 +132,10 @@ protected function assertParentLink($cid, $pid) {
     // <a id="comment-2"></a>
     // <article>
     //   <p class="parent">
-    //     <a href="...comment-1"></a>
+    //     <a href="...comment/1"></a>
     //   </p>
     //  </article>
-    $pattern = "//a[@id='comment-$cid']/following-sibling::article//p[contains(@class, 'parent')]//a[contains(@href, 'comment-$pid')]";
+    $pattern = "//a[@id='comment-$cid']/following-sibling::article//p[contains(@class, 'parent')]//a[contains(@href, 'comment/$pid')]";
 
     $this->assertFieldByXpath($pattern, NULL, format_string(
       'Comment %cid has a link to parent %pid.',
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 7a9ffa9..af51e5f 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
@@ -45,7 +45,12 @@
  *     "bundle" = "type"
  *   },
  *   route_base_path = "admin/structure/types/manage/{bundle}",
- *   permission_granularity = "bundle"
+ *   permission_granularity = "bundle",
+ *   links = {
+ *     "canonical" = "/node/{id}",
+ *     "edit-form" = "/node/{id}/edit",
+ *     "version-history" = "/node/{id}/revisions"
+ *   }
  * )
  */
 class Node extends EntityNG implements NodeInterface {
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 6d7b1c9..ced5e7e 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -2160,10 +2160,15 @@ 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.
+
+  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);
+  }
+
+  $uri = $node->uri('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/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
index dbdba31..b0cb999 100644
--- a/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
+++ b/core/modules/rdf/lib/Drupal/rdf/Tests/CommentAttributesTest.php
@@ -135,11 +135,11 @@ public function testCommentReplyOfRdfaMarkup() {
     $this->drupalLogin($this->web_user);
     $comment_1 = $this->saveComment($this->node->nid, $this->web_user->uid);
 
-    $comment_1_uri = url('comment/' . $comment_1->id(), array('fragment' => 'comment-' . $comment_1->id(), 'absolute' => TRUE));
+    $comment_1_uri = url('comment/' . $comment_1->id(), array('absolute' => TRUE));
 
     // Posts a reply to the first comment.
     $comment_2 = $this->saveComment($this->node->nid, $this->web_user->uid, NULL, $comment_1->id());
-    $comment_2_uri = url('comment/' . $comment_2->id(), array('fragment' => 'comment-' . $comment_2->id(), 'absolute' => TRUE));
+    $comment_2_uri = url('comment/' . $comment_2->id(), array('absolute' => TRUE));
 
     $parser = new \EasyRdf_Parser_Rdfa();
     $graph = new \EasyRdf_Graph();
@@ -176,7 +176,7 @@ public function testCommentReplyOfRdfaMarkup() {
    *   An array containing information about an anonymous user.
    */
   function _testBasicCommentRdfaMarkup($graph, $comment, $account = array()) {
-    $comment_uri = url('comment/' . $comment->id(), array('fragment' => 'comment-' . $comment->id(), 'absolute' => TRUE));
+    $comment_uri = url('comment/' . $comment->id(), array('absolute' => TRUE));
 
     // Comment type.
     $expected_value = array(
diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module
index aaea6bb..535497a 100644
--- a/core/modules/rdf/rdf.module
+++ b/core/modules/rdf/rdf.module
@@ -419,7 +419,7 @@ function rdf_comment_load($comments) {
     $comment->rdf_data['date'] = rdf_rdfa_attributes($comment->rdf_mapping['created'], $comment->created->value);
     $comment->rdf_data['nid_uri'] = url('node/' . $comment->nid->target_id);
     if ($comment->pid->target_id) {
-      $comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id, array('fragment' => 'comment-' . $comment->pid->target_id));
+      $comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id);
     }
   }
 }
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..f18e9b7 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/{id}",
+ *     "edit-form" = "/taxonomy/term/{id}/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..55ed9f1 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -32,11 +32,14 @@ function taxonomy_term_page(Term $term) {
   drupal_set_breadcrumb($breadcrumb);
   drupal_add_feed('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->label());
 
-  $uri = $term->uri();
+  foreach ($term->uriRelationships() as $rel) {
+    $uri = $term->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);
+  }
 
-  // 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.
+  $uri = $term->uri('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));
