diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index da3e02a..eac2c40 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -546,10 +546,10 @@ public static function linkGenerator() {
    *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
+   * @return \Drupal\Core\GeneratedLink
    *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   *   When $collect_bubbleable_metadata is TRUE, the GeneratedLink object has
+   *   bubbleable metadata.
    *
    * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
    * @see \Drupal\Core\Url
diff --git a/core/lib/Drupal/Core/GeneratedLink.php b/core/lib/Drupal/Core/GeneratedLink.php
index eb35bc6..bfd9769 100644
--- a/core/lib/Drupal/Core/GeneratedLink.php
+++ b/core/lib/Drupal/Core/GeneratedLink.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core;
 
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 
 /**
@@ -15,7 +16,7 @@
  * Note: not to be confused with \Drupal\Core\Link, which is for passing around
  *   ungenerated links (typically link text + route name + route parameters).
  */
-class GeneratedLink extends BubbleableMetadata {
+class GeneratedLink extends BubbleableMetadata implements SafeStringInterface {
 
   /**
    * The HTML string value containing a link.
@@ -46,4 +47,18 @@ public function setGeneratedLink($generated_link) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    return (string) $this->generatedLink;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Link.php b/core/lib/Drupal/Core/Link.php
index 0aa9d07..ed541e2 100644
--- a/core/lib/Drupal/Core/Link.php
+++ b/core/lib/Drupal/Core/Link.php
@@ -144,10 +144,8 @@ public function setUrl(Url $url) {
    *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
+   * @return \Drupal\Core\GeneratedLink
    *   The link HTML markup.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
    *
    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
    *   self::toRenderable() instead.
diff --git a/core/lib/Drupal/Core/Menu/menu.api.php b/core/lib/Drupal/Core/Menu/menu.api.php
index 0ae528f..b6ad987 100644
--- a/core/lib/Drupal/Core/Menu/menu.api.php
+++ b/core/lib/Drupal/Core/Menu/menu.api.php
@@ -440,7 +440,11 @@ function hook_system_breadcrumb_alter(\Drupal\Core\Breadcrumb\Breadcrumb &$bread
  *   exposed as the 'link_generator' service or a link generated by _l(). If the
  *   link is a "route link", 'route_name' will be set, otherwise 'path' will be
  *   set. The following keys can be altered:
- *   - text: The link text for the anchor tag as a translated string.
+ *   - text: The link text for the anchor tag. If the hook implementation
+ *     changes this text is needs to preserve the safeness of the original text.
+ *     Using t() or \Drupal\Component\Utility\SafeMarkup::format() with
+ *     @placeholder is recommended as this will escape the original text if
+ *     necessary. If the resulting text is not marked safe it will be escaped.
  *   - url_is_active: Whether or not the link points to the currently active
  *     URL.
  *   - url: The \Drupal\Core\Url object.
@@ -468,7 +472,7 @@ function hook_system_breadcrumb_alter(\Drupal\Core\Breadcrumb\Breadcrumb &$bread
 function hook_link_alter(&$variables) {
   // Add a warning to the end of route links to the admin section.
   if (isset($variables['route_name']) && strpos($variables['route_name'], 'admin') !== FALSE) {
-    $variables['text'] .= ' (Warning!)';
+    $variables['text'] = t('@text (Warning!)', ['@text' => $variables['text']]);
   }
 }
 
diff --git a/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php b/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
index 35170d1..a199288 100644
--- a/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
+++ b/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
@@ -35,7 +35,7 @@
    * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate() for details
    *   on the arguments, usage, and possible exceptions.
    *
-   * @return string
+   * @return \Drupal\Core\GeneratedLink
    *   An HTML string containing a link to the given route and parameters.
    */
   protected function l($text, Url $url) {
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index a595394..c920e28 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Utility;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -15,6 +16,7 @@
 use Drupal\Core\Link;
 use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
@@ -85,9 +87,13 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE)
     // service from the container.
     $url->setUrlGenerator($this->urlGenerator);
 
+    if (is_array($text)) {
+      $text = $this->renderer->render($text);
+    }
+
     // Start building a structured representation of our link to be altered later.
     $variables = array(
-      'text' => is_array($text) ? $this->renderer->render($text) : $text,
+      'text' => $text,
       'url' => $url,
       'options' => $url->getOptions(),
     );
@@ -151,6 +157,7 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE)
 
     if (!$collect_bubbleable_metadata) {
       $url_string = $url->toString($collect_bubbleable_metadata);
+      $generated_link = new GeneratedLink();
     }
     else {
       $generated_url = $url->toString($collect_bubbleable_metadata);
@@ -161,9 +168,13 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE)
     // attribute, and it is escaped by \Drupal\Core\Template\Attribute.
     $attributes['href'] = $url_string;
 
-    $result = SafeMarkup::format('<a@attributes>@text</a>', array('@attributes' => new Attribute($attributes), '@text' => $variables['text']));
-
-    return $collect_bubbleable_metadata ? $generated_link->setGeneratedLink($result) : $result;
+    if (!SafeMarkup::isSafe($variables['text'])) {
+      $variables['text'] = Html::escape($variables['text']);
+    }
+    $attributes = new Attribute($attributes);
+    // This is safe because Attribute does escaping and $variables['text'] is
+    // either rendered or escaped.
+    return $generated_link->setGeneratedLink('<a' . $attributes . '>' . $variables['text'] . '</a>');
   }
 
 }
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 733c449..00d2566 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -64,10 +64,10 @@
    *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
+   * @return \Drupal\Core\GeneratedLink
    *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   *   When $collect_bubbleable_metadata is TRUE, the GeneratedLink object has
+   *   bubbleable metadata.
    *
    * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
    *   Thrown when the named route doesn't exist.
@@ -88,10 +88,10 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE);
    *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
+   * @return \Drupal\Core\GeneratedLink
    *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   *   When $collect_bubbleable_metadata is TRUE, the GeneratedLink object has
+   *   bubbleable metadata.
    */
   public function generateFromLink(Link $link, $collect_bubbleable_metadata = FALSE);
 
diff --git a/core/modules/system/src/Tests/Utility/LinkGenerationTest.php b/core/modules/system/src/Tests/Utility/LinkGenerationTest.php
new file mode 100644
index 0000000..62c4d23
--- /dev/null
+++ b/core/modules/system/src/Tests/Utility/LinkGenerationTest.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Utility\LinkGenerationTest.
+ */
+
+namespace Drupal\system\Tests\Utility;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Url;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests link generation with hooks.
+ *
+ * @group Utility
+ */
+class LinkGenerationTest extends KernelTestBase {
+
+  public static $modules = ['link_generation_test'];
+
+  /**
+   * Tests how hook_link_alter() can affect escaping of the link text.
+   */
+  function testHookLinkAlter() {
+    $url = Url::fromUri('http://example.com');
+    $renderer = \Drupal::service('renderer');
+
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is not escaped.
+    $this->assertRaw('<em>link with markup</em>');
+
+    // Test just adding text to an already safe string.
+    \Drupal::state()->set('link_generation_test_link_alter', TRUE);
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertEscaped('<em>link with markup</em> <strong>Test!</strong>');
+
+    // Test passing a safe string to t().
+    \Drupal::state()->set('link_generation_test_link_alter_safe', TRUE);
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertRaw('<em>link with markup</em> <strong>Test!</strong>');
+
+    // Test passing an unsafe string to t().
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l('<em>link with markup</em>', $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertEscaped('<em>link with markup</em>');
+    $this->assertRaw('<strong>Test!</strong>');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml b/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml
new file mode 100644
index 0000000..2f200a4
--- /dev/null
+++ b/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Link generation test support'
+type: module
+description: 'Test hooks fired in link generation.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/link_generation_test/link_generation_test.module b/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
new file mode 100644
index 0000000..622a403
--- /dev/null
+++ b/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * Helper module for the link generation tests.
+ */
+
+/**
+ * Implements hook_link_alter().
+ */
+function link_generation_test_link_alter(&$variables) {
+  if (\Drupal::state()->get('link_generation_test_link_alter', FALSE)) {
+    // Add a text to the end of links.
+    if (\Drupal::state()->get('link_generation_test_link_alter_safe', FALSE)) {
+      $variables['text'] = t('@text <strong>Test!</strong>', ['@text' => $variables['text']]);
+    }
+    else {
+      $variables['text'] .= ' <strong>Test!</strong>';
+    }
+  }
+}
diff --git a/core/modules/views_ui/src/Controller/ViewsUIController.php b/core/modules/views_ui/src/Controller/ViewsUIController.php
index 2837508..be71932 100644
--- a/core/modules/views_ui/src/Controller/ViewsUIController.php
+++ b/core/modules/views_ui/src/Controller/ViewsUIController.php
@@ -121,13 +121,15 @@ public function reportFields() {
   public function reportPlugins() {
     $rows = Views::pluginList();
     foreach ($rows as &$row) {
+      $views = [];
       // Link each view name to the view itself.
       foreach ($row['views'] as $row_name => $view) {
-        $row['views'][$row_name] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
+        $views[] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
       }
+      unset($row['views']);
       $row['views']['data'] = [
         '#theme' => 'item_list',
-        '#items' => $row['views'],
+        '#items' => $views,
         '#context' => ['list_style' => 'comma-list'],
       ];
     }
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index 6659945..4d1cee0 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Utility {
 
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Link;
@@ -488,16 +489,16 @@ public function testGenerateBubbleableMetadata() {
     $expected_link_markup = '<a href="/test-route-1">Test</a>';
 
     // Test ::generate().
-    $this->assertSame($expected_link_markup, $this->linkGenerator->generate('Test', $url));
+    $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url));
     $generated_link = $this->linkGenerator->generate('Test', $url, TRUE);
-    $this->assertSame($expected_link_markup, $generated_link->getGeneratedLink());
+    $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
 
     // Test ::generateFromLink().
     $link = new Link('Test', $url);
-    $this->assertSame($expected_link_markup, $this->linkGenerator->generateFromLink($link));
+    $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generateFromLink($link));
     $generated_link = $this->linkGenerator->generateFromLink($link, TRUE);
-    $this->assertSame($expected_link_markup, $generated_link->getGeneratedLink());
+    $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
   }
 
@@ -508,12 +509,12 @@ public function testGenerateBubbleableMetadata() {
    *   An associative array of link properties, with the following keys:
    *   - attributes: optional array of HTML attributes that should be present.
    *   - content: optional link content.
-   * @param string $html
+   * @param \Drupal\Component\Utility\SafeStringInterface $html
    *   The HTML to check.
    * @param int $count
    *   How many times the link should be present in the HTML. Defaults to 1.
    */
-  public static function assertLink(array $properties, $html, $count = 1) {
+  public static function assertLink(array $properties, SafeStringInterface $html, $count = 1) {
     // Provide default values.
     $properties += array('attributes' => array());
 
