diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
index 983eec85fe..06c5cce5c5 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -3,10 +3,12 @@
 namespace Drupal\Core\Entity\Controller;
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -15,6 +17,13 @@
 class EntityViewController implements ContainerInjectionInterface {
 
   /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
    * The entity manager
    *
    * @var \Drupal\Core\Entity\EntityManagerInterface
@@ -35,10 +44,14 @@ class EntityViewController implements ContainerInjectionInterface {
    *   The entity manager.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user. For backwards compatibility this is optional, however
+   *   this will be removed before Drupal 9.0.0.
    */
-  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer) {
+  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AccountInterface $current_user = NULL) {
     $this->entityManager = $entity_manager;
     $this->renderer = $renderer;
+    $this->currentUser = $current_user ?: \Drupal::currentUser();
   }
 
   /**
@@ -61,16 +74,25 @@ public static function create(ContainerInterface $container) {
    *   The changed page render array.
    */
   public function buildTitle(array $page) {
-    $entity_type = $page['#entity_type'];
-    $entity = $page['#' . $entity_type];
+    $entity_output = $page['entity'];
+    $entity_type = $entity_output['#entity_type'];
+    $entity = $entity_output['#' . $entity_type];
+
     // If the entity's label is rendered using a field formatter, set the
     // rendered title field formatter as the page title instead of the default
     // plain text title. This allows attributes set on the field to propagate
     // correctly (e.g. RDFa, in-place editing).
     if ($entity instanceof FieldableEntityInterface) {
       $label_field = $entity->getEntityType()->getKey('label');
-      if (isset($page[$label_field])) {
-        $page['#title'] = $this->renderer->render($page[$label_field]);
+      $view_mode = isset($entity_output['#view_mode']) ? $entity_output['#view_mode'] : 'full';
+      $displays = EntityViewDisplay::collectRenderDisplays([$entity], $view_mode);
+      $builds = [$entity_output];
+      $this->entityManager
+        ->getViewBuilder($entity->getEntityTypeId())
+        ->buildComponents($builds, [$entity], $displays, $view_mode);
+      $entity_build = reset($builds);
+      if (isset($entity_build[$label_field])) {
+        $page['#title'] = $this->renderer->render($entity_build[$label_field]);
       }
     }
     return $page;
@@ -91,13 +113,52 @@ public function buildTitle(array $page) {
    *   A render array as expected by drupal_render().
    */
   public function view(EntityInterface $_entity, $view_mode = 'full') {
-    $page = $this->entityManager
+    $page['entity'] = $this->entityManager
       ->getViewBuilder($_entity->getEntityTypeId())
       ->view($_entity, $view_mode);
 
     $page['#pre_render'][] = [$this, 'buildTitle'];
-    $page['#entity_type'] = $_entity->getEntityTypeId();
-    $page['#' . $page['#entity_type']] = $_entity;
+    $page['entity']['#entity_type'] = $_entity->getEntityTypeId();
+    $page['entity']['#' . $page['entity']['#entity_type']] = $_entity;
+
+    foreach ($_entity->uriRelationships() as $rel) {
+      $url = $_entity->toUrl($rel);
+      // Add link relationships if the user is authenticated or if the anonymous
+      // user has access. Access checking must be done for anonymous users to
+      // avoid traffic to inaccessible pages from web crawlers. For
+      // authenticated users, showing the links in HTML head does not impact
+      // user experience or security, since the routes are access checked when
+      // visited and only visible via view source. This prevents doing
+      // potentially expensive and hard to cache access checks on every request.
+      // This means that the page will vary by user.permissions. We also rely on
+      // the access checking fallback to ensure the correct cacheability
+      // metadata if we have to check access.
+      if ($this->currentUser->isAuthenticated() || $url->access($this->currentUser)) {
+        // Set the entity path as the canonical URL to prevent duplicate content.
+        $page['#attached']['html_head_link'][] = [
+          [
+            'rel' => $rel,
+            'href' => $url->toString(),
+          ],
+          TRUE,
+        ];
+      }
+
+      if ($rel === 'canonical') {
+        // Set the non-aliased canonical path as a default shortlink.
+        $page['#attached']['html_head_link'][] = [
+          [
+            'rel' => 'shortlink',
+            'href' => $url->setOption('alias', TRUE)->toString(),
+          ],
+          TRUE,
+        ];
+      }
+    }
+
+    // Given this varies by $this->currentUser->isAuthenticated(), add a cache
+    // context based on the anonymous role.
+    $page['#cache']['contexts'][] = 'user.roles:anonymous';
 
     return $page;
   }
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 95e85e21a4..a4288701e3 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -183,10 +183,16 @@ public function toUrl($rel = 'canonical', array $options = []) {
     // The links array might contain URI templates set in annotations.
     $link_templates = $this->linkTemplates();
 
-    // Links pointing to the current revision point to the actual entity. So
-    // instead of using the 'revision' link, use the 'canonical' link.
-    if ($rel === 'revision' && $this instanceof RevisionableInterface && $this->isDefaultRevision()) {
-      $rel = 'canonical';
+    if ($rel === 'revision' && $this instanceof RevisionableInterface) {
+      // Links pointing to the current revision point to the actual entity. So
+      // instead of using the 'revision' link, use the 'canonical' link.
+      if ($this->isDefaultRevision()) {
+        $rel = 'canonical';
+      }
+      elseif ($this->isNewRevision()) {
+        // Unsaved revisions cannot be linked to.
+        throw new EntityMalformedException(sprintf('The "%s" entity cannot have a revision URI as it is an unsaved revision yet.', $this->getEntityTypeId()));
+      }
     }
 
     if (isset($link_templates[$rel])) {
@@ -332,6 +338,10 @@ public function uriRelationships() {
       try {
         $this->toUrl($link_relation_type)->toString(TRUE)->getGeneratedUrl();
       }
+      catch (EntityMalformedException $e) {
+        // New entities that are previewed do not yet have an ID.
+        return FALSE;
+      }
       catch (RouteNotFoundException $e) {
         return FALSE;
       }
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
index e7a7dfc9dd..357244b99a 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
@@ -51,7 +51,7 @@
    *
    * @var string[]
    */
-  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions'];
+  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions', 'user.roles:anonymous'];
 
   /**
    * Tests the basic translation UI.
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 48401475fc..d9fc952309 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Controller\EntityViewController;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Url;
 use Drupal\node\NodeStorageInterface;
@@ -127,7 +128,7 @@ public function add(NodeTypeInterface $node_type) {
   public function revisionShow($node_revision) {
     $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision);
     $node = $this->entityManager()->getTranslationFromContext($node);
-    $node_view_controller = new NodeViewController($this->entityManager, $this->renderer, $this->currentUser());
+    $node_view_controller = new EntityViewController($this->entityManager, $this->renderer, $this->currentUser());
     $page = $node_view_controller->view($node);
     unset($page['nodes'][$node->id()]['#cache']);
     return $page;
diff --git a/core/modules/node/src/Controller/NodeViewController.php b/core/modules/node/src/Controller/NodeViewController.php
deleted file mode 100644
index b8aa1e5a97..0000000000
--- a/core/modules/node/src/Controller/NodeViewController.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace Drupal\node\Controller;
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\Controller\EntityViewController;
-use Drupal\Core\Entity\EntityManagerInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Defines a controller to render a single node.
- */
-class NodeViewController extends EntityViewController {
-
-  /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $currentUser;
-
-  /**
-   * Creates an NodeViewController object.
-   *
-   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
-   *   The entity manager.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer service.
-   * @param \Drupal\Core\Session\AccountInterface $current_user
-   *   The current user. For backwards compatibility this is optional, however
-   *   this will be removed before Drupal 9.0.0.
-   */
-  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AccountInterface $current_user = NULL) {
-    parent::__construct($entity_manager, $renderer);
-    $this->currentUser = $current_user ?: \Drupal::currentUser();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity.manager'),
-      $container->get('renderer'),
-      $container->get('current_user')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function view(EntityInterface $node, $view_mode = 'full', $langcode = NULL) {
-    $build = parent::view($node, $view_mode, $langcode);
-
-    foreach ($node->uriRelationships() as $rel) {
-      $url = $node->toUrl($rel);
-      // Add link relationships if the user is authenticated or if the anonymous
-      // user has access. Access checking must be done for anonymous users to
-      // avoid traffic to inaccessible pages from web crawlers. For
-      // authenticated users, showing the links in HTML head does not impact
-      // user experience or security, since the routes are access checked when
-      // visited and only visible via view source. This prevents doing
-      // potentially expensive and hard to cache access checks on every request.
-      // This means that the page will vary by user.permissions. We also rely on
-      // the access checking fallback to ensure the correct cacheability
-      // metadata if we have to check access.
-      if ($this->currentUser->isAuthenticated() || $url->access($this->currentUser)) {
-        // Set the node path as the canonical URL to prevent duplicate content.
-        $build['#attached']['html_head_link'][] = [
-          [
-            'rel' => $rel,
-            'href' => $url->toString(),
-          ],
-          TRUE,
-        ];
-      }
-
-      if ($rel == 'canonical') {
-        // Set the non-aliased canonical path as a default shortlink.
-        $build['#attached']['html_head_link'][] = [
-          [
-            'rel' => 'shortlink',
-            'href' => $url->setOption('alias', TRUE)->toString(),
-          ],
-          TRUE,
-        ];
-      }
-    }
-
-    // Given this varies by $this->currentUser->isAuthenticated(), add a cache
-    // context based on the anonymous role.
-    $build['#cache']['contexts'][] = 'user.roles:anonymous';
-
-    return $build;
-  }
-
-  /**
-   * The _title_callback for the page that renders a single node.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $node
-   *   The current node.
-   *
-   * @return string
-   *   The page title.
-   */
-  public function title(EntityInterface $node) {
-    return $this->entityManager->getTranslationFromContext($node)->label();
-  }
-
-}
diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php
index 0803f1ef14..0ef0b5f338 100644
--- a/core/modules/node/src/Entity/NodeRouteProvider.php
+++ b/core/modules/node/src/Entity/NodeRouteProvider.php
@@ -3,6 +3,7 @@
 namespace Drupal\node\Entity;
 
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
 use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Routing\RouteCollection;
@@ -10,40 +11,28 @@
 /**
  * Provides routes for nodes.
  */
-class NodeRouteProvider implements EntityRouteProviderInterface {
+class NodeRouteProvider extends DefaultHtmlRouteProvider {
 
   /**
    * {@inheritdoc}
    */
-  public function getRoutes( EntityTypeInterface $entity_type) {
-    $route_collection = new RouteCollection();
-    $route = (new Route('/node/{node}'))
-      ->addDefaults([
-        '_controller' => '\Drupal\node\Controller\NodeViewController::view',
-        '_title_callback' => '\Drupal\node\Controller\NodeViewController::title',
-      ])
-      ->setRequirement('node', '\d+')
-      ->setRequirement('_entity_access', 'node.view');
-    $route_collection->add('entity.node.canonical', $route);
-
-    $route = (new Route('/node/{node}/delete'))
-      ->addDefaults([
-        '_entity_form' => 'node.delete',
-        '_title' => 'Delete',
-      ])
-      ->setRequirement('node', '\d+')
-      ->setRequirement('_entity_access', 'node.delete')
-      ->setOption('_node_operation_route', TRUE);
-    $route_collection->add('entity.node.delete_form', $route);
-
-    $route = (new Route('/node/{node}/edit'))
-      ->setDefault('_entity_form', 'node.edit')
-      ->setRequirement('_entity_access', 'node.update')
-      ->setRequirement('node', '\d+')
-      ->setOption('_node_operation_route', TRUE);
-    $route_collection->add('entity.node.edit_form', $route);
-
-    return $route_collection;
+  protected function getEditFormRoute(EntityTypeInterface $entity_type) {
+    $route = parent::getEditFormRoute($entity_type);
+
+    $route->setOption('_node_operation_route', TRUE);
+
+    return $route;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
+    $route = parent::getDeleteFormRoute($entity_type);
+
+    $route->setOption('_node_operation_route', TRUE);
+
+    return $route;
   }
 
 }
diff --git a/core/modules/node/tests/src/Functional/NodeCacheTagsTest.php b/core/modules/node/tests/src/Functional/NodeCacheTagsTest.php
index ca5a0a9912..fc8cdf2dd3 100644
--- a/core/modules/node/tests/src/Functional/NodeCacheTagsTest.php
+++ b/core/modules/node/tests/src/Functional/NodeCacheTagsTest.php
@@ -43,7 +43,7 @@ protected function createEntity() {
    */
   protected function getDefaultCacheContexts() {
     $defaults = parent::getDefaultCacheContexts();
-    // @see \Drupal\node\Controller\NodeViewController::view()
+    // @see \Drupal\Core\Entity\Controller\EntityViewController::view()
     $defaults[] = 'user.roles:anonymous';
     return $defaults;
   }
diff --git a/core/modules/statistics/tests/src/Functional/StatisticsReportsTest.php b/core/modules/statistics/tests/src/Functional/StatisticsReportsTest.php
index edebca166c..73dc0325c0 100644
--- a/core/modules/statistics/tests/src/Functional/StatisticsReportsTest.php
+++ b/core/modules/statistics/tests/src/Functional/StatisticsReportsTest.php
@@ -53,7 +53,7 @@ public function testPopularContentBlock() {
     $tags = Cache::mergeTags($tags, ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']);
     $this->assertCacheTags($tags);
     $contexts = Cache::mergeContexts($node->getCacheContexts(), $block->getCacheContexts());
-    $contexts = Cache::mergeContexts($contexts, ['url.query_args:_wrapper_format']);
+    $contexts = Cache::mergeContexts($contexts, ['url.query_args:_wrapper_format', 'user.roles:anonymous']);
     $this->assertCacheContexts($contexts);
 
     // Check if the node link is displayed.
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 4a93989ad5..7860fdf4c4 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -96,37 +96,6 @@ function taxonomy_term_uri($term) {
 }
 
 /**
- * Implements hook_page_attachments_alter().
- */
-function taxonomy_page_attachments_alter(array &$page) {
-  $route_match = \Drupal::routeMatch();
-  if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
-    foreach ($term->uriRelationships() as $rel) {
-      // Set the URI relationships, like canonical.
-      $page['#attached']['html_head_link'][] = [
-        [
-          'rel' => $rel,
-          'href' => $term->url($rel),
-        ],
-        TRUE,
-      ];
-
-      // Set the term path as the canonical URL to prevent duplicate content.
-      if ($rel == 'canonical') {
-        // Set the non-aliased canonical path as a default shortlink.
-        $page['#attached']['html_head_link'][] = [
-          [
-            'rel' => 'shortlink',
-            'href' => $term->url($rel, ['alias' => TRUE]),
-          ],
-          TRUE,
-        ];
-      }
-    }
-  }
-}
-
-/**
  * Implements hook_theme().
  */
 function taxonomy_theme() {
diff --git a/core/modules/user/src/Entity/UserRouteProvider.php b/core/modules/user/src/Entity/UserRouteProvider.php
index d1e967184e..5c2bde6b50 100644
--- a/core/modules/user/src/Entity/UserRouteProvider.php
+++ b/core/modules/user/src/Entity/UserRouteProvider.php
@@ -3,6 +3,7 @@
 namespace Drupal\user\Entity;
 
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
 use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Routing\RouteCollection;
@@ -10,31 +11,30 @@
 /**
  * Provides routes for the user entity.
  */
-class UserRouteProvider implements EntityRouteProviderInterface {
+class UserRouteProvider extends DefaultHtmlRouteProvider {
 
   /**
    * {@inheritdoc}
    */
-  public function getRoutes(EntityTypeInterface $entity_type) {
-    $route_collection = new RouteCollection();
-    $route = (new Route('/user/{user}'))
-      ->setDefaults([
-        '_entity_view' => 'user.full',
-        '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
-      ])
-      ->setRequirement('user', '\d+')
-      ->setRequirement('_entity_access', 'user.view');
-    $route_collection->add('entity.user.canonical', $route);
+  protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
+    return parent::getCanonicalRoute($entity_type)
+      ->setDefault('_title_callback', 'Drupal\user\Controller\UserController::userTitle');
+  }
 
-    $route = (new Route('/user/{user}/edit'))
-      ->setDefaults([
-        '_entity_form' => 'user.default',
-        '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
-      ])
-      ->setOption('_admin_route', TRUE)
-      ->setRequirement('user', '\d+')
-      ->setRequirement('_entity_access', 'user.update');
-    $route_collection->add('entity.user.edit_form', $route);
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditFormRoute(EntityTypeInterface $entity_type) {
+    return parent::getEditFormRoute($entity_type)
+      ->setDefault('_title_callback', 'Drupal\user\Controller\UserController::userTitle')
+      ->setOption('_admin_route', TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $route_collection = parent::getRoutes($entity_type);
 
     $route = (new Route('/user/{user}/cancel'))
       ->setDefaults([
