 core/modules/book/book.module                      |   74 ++++++-------
 .../modules/node/lib/Drupal/node/NodeInterface.php |    2 +-
 .../node/lib/Drupal/node/NodeViewBuilder.php       |  110 +++++++++++++++-----
 core/modules/node/node.api.php                     |   32 ++++++
 core/modules/statistics/statistics.module          |   23 ++--
 5 files changed, 158 insertions(+), 83 deletions(-)

diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 94c75b0..725b010 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -6,13 +6,13 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\NodeInterface;
 use Drupal\node\NodeTypeInterface;
 use Drupal\Core\Language\Language;
 use Drupal\entity\Entity\EntityDisplay;
 use Drupal\Core\Template\Attribute;
 use Drupal\menu_link\Entity\MenuLink;
 use Drupal\menu_link\MenuLinkStorageController;
-use Drupal\node\NodeInterface;
 
 /**
  * Implements hook_help().
@@ -121,45 +121,41 @@ function book_entity_info(&$entity_info) {
 }
 
 /**
- * Adds relevant book links to the node's links.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   The book page node to add links to.
- * @param $view_mode
- *   The view mode of the node.
- */
-function book_node_view_link(NodeInterface $node, $view_mode) {
-  $links = array();
-  $account = \Drupal::currentUser();
-
-  if (isset($node->book['depth'])) {
-    if ($view_mode == 'full' && node_is_page($node)) {
-      $child_type = \Drupal::config('book.settings')->get('child_type');
-      $access_controller = Drupal::entityManager()->getAccessController('node');
-      if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_controller->createAccess($child_type) && $node->isPublished() && $node->book['depth'] < MENU_MAX_DEPTH) {
-        $links['book_add_child'] = array(
-          'title' => t('Add child page'),
-          'href' => 'node/add/' . $child_type,
-          'query' => array('parent' => $node->book['mlid']),
-        );
-      }
-
-      if ($account->hasPermission('access printer-friendly version')) {
-        $links['book_printer'] = array(
-          'title' => t('Printer-friendly version'),
-          'href' => 'book/export/html/' . $node->id(),
-          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
-        );
+ * Implements hook_node_links_alter().
+ */
+function book_node_links_alter(array &$node_links, NodeInterface $node, array &$context) {
+  if ($context['view_mode'] != 'rss') {
+    $account = \Drupal::currentUser();
+
+    if (isset($node->book['depth'])) {
+      if ($context['view_mode'] == 'full' && node_is_page($node)) {
+        $child_type = \Drupal::config('book.settings')->get('child_type');
+        $access_controller = \Drupal::entityManager()->getAccessController('node');
+        if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_controller->createAccess($child_type) && $node->isPublished() && $node->book['depth'] < MENU_MAX_DEPTH) {
+          $links['book_add_child'] = array(
+            'title' => t('Add child page'),
+            'href' => 'node/add/' . $child_type,
+            'query' => array('parent' => $node->book['mlid']),
+          );
+        }
+
+        if ($account->hasPermission('access printer-friendly version')) {
+          $links['book_printer'] = array(
+            'title' => t('Printer-friendly version'),
+            'href' => 'book/export/html/' . $node->id(),
+            'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
+          );
+        }
       }
     }
-  }
 
-  if (!empty($links)) {
-    $node->content['links']['book'] = array(
-      '#theme' => 'links__node__book',
-      '#links' => $links,
-      '#attributes' => array('class' => array('links', 'inline')),
-    );
+    if (!empty($links)) {
+      $node_links['book'] = array(
+        '#theme' => 'links__node__book',
+        '#links' => $links,
+        '#attributes' => array('class' => array('links', 'inline')),
+      );
+    }
   }
 }
 
@@ -467,10 +463,6 @@ function book_node_view(EntityInterface $node, EntityDisplay $display, $view_mod
       );
     }
   }
-
-  if ($view_mode != 'rss') {
-    book_node_view_link($node, $view_mode);
-  }
 }
 
 /**
diff --git a/core/modules/node/lib/Drupal/node/NodeInterface.php b/core/modules/node/lib/Drupal/node/NodeInterface.php
index 34f7274..b4906af 100644
--- a/core/modules/node/lib/Drupal/node/NodeInterface.php
+++ b/core/modules/node/lib/Drupal/node/NodeInterface.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\node\Entity\NodeInterface.
+ * Contains \Drupal\node\NodeInterface.
  */
 
 namespace Drupal\node;
diff --git a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
index 036d283..17030d0 100644
--- a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
+++ b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php
@@ -35,33 +35,14 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
       $display = $displays[$bundle];
 
       $entity->content['links'] = array(
-        '#theme' => 'links__node',
-        '#pre_render' => array('drupal_pre_render_links'),
-        '#attributes' => array('class' => array('links', 'inline')),
-      );
-
-      // Always display a read more link on teasers because we have no way
-      // to know when a teaser view is different than a full view.
-      $links = array();
-      if ($view_mode == 'teaser') {
-        $node_title_stripped = strip_tags($entity->label());
-        $links['node-readmore'] = array(
-          'title' => t('Read more<span class="visually-hidden"> about @title</span>', array(
-            '@title' => $node_title_stripped,
-          )),
-          'href' => 'node/' . $entity->id(),
-          'html' => TRUE,
-          'attributes' => array(
-            'rel' => 'tag',
-            'title' => $node_title_stripped,
-          ),
-        );
-      }
-
-      $entity->content['links']['node'] = array(
-        '#theme' => 'links__node__node',
-        '#links' => $links,
-        '#attributes' => array('class' => array('links', 'inline')),
+        '#type' => 'render_cache_placeholder',
+        '#callback' => '\Drupal\node\NodeViewBuilder::renderLinks',
+        '#context' => array(
+          'node_entity_id' => $entity->id(),
+          'view_mode' => $view_mode,
+          'langcode' => $langcode,
+          'in_preview' => !empty($entity->in_preview),
+        ),
       );
 
       // Add Language field text element to node render array.
@@ -78,6 +59,81 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
   }
 
   /**
+   * #post_render_cache callback; replaces the placeholder with node links.
+   *
+   * Renders the links on a node.
+   *
+   * @param array $context
+   *   An array with the following keys:
+   *   - node_entity_id: a node entity ID
+   *   - view_mode: the view mode in which the node entity is being viewed
+   *   - langcode: in which language the node entity is being viewed
+   *   - in_preview: whether the node is currently being previewed
+   *
+   * @return array
+   *   A renderable array representing the node links.
+   */
+  public static function renderLinks(array $context) {
+    $links = array(
+      '#theme' => 'links__node',
+      '#pre_render' => array('drupal_pre_render_links'),
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
+
+    if (!$context['in_preview']) {
+      $entity = entity_load('node', $context['node_entity_id']);
+      $links['node'] = self::buildLinks($entity, $context['view_mode']);
+
+      // Allow other modules to alter the node links.
+      $hook_context = array(
+        'view_mode' => $context['view_mode'],
+        'langcode' => $context['langcode'],
+      );
+      \Drupal::moduleHandler()->alter('node_links', $links, $entity, $hook_context);
+    }
+
+    return $links;
+  }
+
+  /**
+   * Build the default links (Read more) for a node.
+   *
+   * @param \Drupal\node\NodeInterface $entity
+   *   The node object.
+   * @param string $view_mode
+   *   A view mode identifier.
+   *
+   * @return array
+   *   An array that can be processed by drupal_pre_render_links().
+   */
+  protected static function buildLinks(NodeInterface $entity, $view_mode) {
+    $links = array();
+
+    // Always display a read more link on teasers because we have no way
+    // to know when a teaser view is different than a full view.
+    if ($view_mode == 'teaser') {
+      $node_title_stripped = strip_tags($entity->label());
+      $links['node-readmore'] = array(
+        'title' => t('Read more<span class="visually-hidden"> about @title</span>', array(
+          '@title' => $node_title_stripped,
+        )),
+        'href' => 'node/' . $entity->id(),
+        'html' => TRUE,
+        'attributes' => array(
+          'rel' => 'tag',
+          'title' => $node_title_stripped,
+        ),
+      );
+    }
+
+    return array(
+      '#theme' => 'links__node__node',
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
+  }
+
+  /**
    * {@inheritdoc}
    */
   protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) {
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index ba67aca..9c2f1df 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -1,6 +1,7 @@
 <?php
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\Entity\NodeInterface;
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Xss;
 
@@ -940,5 +941,36 @@ function hook_node_type_delete(\Drupal\node\NodeTypeInterface $type) {
 }
 
 /**
+ * Alter the links of a node.
+ *
+ * @param array &$links
+ *   A renderable array representing the node links.
+ * @param \Drupal\node\NodeInterface $entity
+ *   The node being rendered.
+ * @param array &$context
+ *   Various aspects of the context in which the node links are going to be
+ *   displayed, with the following keys:
+ *   - 'view_mode': the view mode in which the comment is being viewed
+ *   - 'langcode': the language in which the comment is being viewed
+ *
+ * @see \Drupal\node\NodeViewBuilder::renderLinks()
+ * @see \Drupal\node\NodeViewBuilder::buildLinks()
+ */
+function hook_node_links_alter(array &$links, NodeInterface $entity, array &$context) {
+  $links['mymodule'] = array(
+    '#theme' => 'links__node__mymodule',
+    '#attributes' => array('class' => array('links', 'inline')),
+    '#links' => array(
+      'node-report' => array(
+        'title' => t('Report'),
+        'href' => "node/{$entity->id()}/report",
+        'html' => TRUE,
+        'query' => array('token' => \Drupal::getContainer()->get('csrf_token')->get("node/{$entity->id()}/report")),
+      ),
+    ),
+  );
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index d2dfb4c..f0393be 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\NodeInterface;
 use Drupal\entity\Entity\EntityDisplay;
 
 /**
@@ -56,13 +57,18 @@ function statistics_node_view(EntityInterface $node, EntityDisplay $display, $vi
       'type' => 'setting',
     );
   }
+}
 
-  if ($view_mode != 'rss') {
+/**
+ * Implements hook_node_links_alter().
+ */
+function statistics_node_links_alter(array &$node_links, NodeInterface $entity, array &$context) {
+  if ($context['view_mode'] != 'rss') {
     if (\Drupal::currentUser()->hasPermission('view post access counter')) {
-      $statistics = statistics_get($node->id());
+      $statistics = statistics_get($entity->id());
       if ($statistics) {
         $links['statistics_counter']['title'] = format_plural($statistics['totalcount'], '1 view', '@count views');
-        $node->content['links']['statistics'] = array(
+        $node_links['statistics'] = array(
           '#theme' => 'links__node__statistics',
           '#links' => $links,
           '#attributes' => array('class' => array('links', 'inline')),
@@ -73,17 +79,6 @@ function statistics_node_view(EntityInterface $node, EntityDisplay $display, $vi
 }
 
 /**
- * Implements hook_node_view_alter().
- */
-function statistics_node_view_alter(&$build, EntityInterface $node, EntityDisplay $display) {
-  // If statistics were added to the node render array, we can't use the render
-  // cache.
-  if (isset($build['links']['statistics'])) {
-    unset($build['#cache']);
-  }
-}
-
-/**
  * Implements hook_menu().
  */
 function statistics_menu() {
