diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 10f546dfd52e56c5b592c2f6eb8efb31767752e5..b007b5543885e095e8902cb0a225c9e62dacecca 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -9,6 +9,7 @@
 use Drupal\content_translation\BundleTranslationSettingsInterface;
 use Drupal\content_translation\ContentTranslationManager;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
@@ -656,6 +657,7 @@ function content_translation_preprocess_language_content_settings_table(&$variab
  * Implements hook_page_attachments().
  */
 function content_translation_page_attachments(&$page) {
+  $cache = CacheableMetadata::createFromRenderArray($page);
   $route_match = \Drupal::routeMatch();
 
   // If the current route has no parameters, return.
@@ -673,6 +675,13 @@ function content_translation_page_attachments(&$page) {
     if ($entity instanceof ContentEntityInterface && $entity->hasLinkTemplate('canonical')) {
       // Current route represents a content entity. Build hreflang links.
       foreach ($entity->getTranslationLanguages() as $language) {
+        // Skip any translation that cannot be viewed.
+        $translation = $entity->getTranslation($language->getId());
+        $access = $translation->access('view', NULL, TRUE);
+        $cache->addCacheableDependency($access);
+        if (!$access->isAllowed()) {
+          continue;
+        }
         $url = $entity->toUrl('canonical')
           ->setOption('language', $language)
           ->setAbsolute()
@@ -688,6 +697,8 @@ function content_translation_page_attachments(&$page) {
       }
     }
     // Since entity was found, no need to iterate further.
-    return;
+    break;
   }
+  // Apply updated caching information.
+  $cache->applyTo($page);
 }
diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
index 697be9a6dec9de62fcf977fbe75096be90867e39..9ac2876220f95fbb1169b1d05b7acc1b889f87ec 100644
--- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
+++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
@@ -371,7 +371,7 @@ public function testTranslationRendering() {
     $this->doTestTranslations('node/' . $node->id(), $values);
 
     // Test that the node page has the correct alternate hreflang links.
-    $this->doTestAlternateHreflangLinks($node->toUrl());
+    $this->doTestAlternateHreflangLinks($node);
   }
 
   /**
@@ -393,24 +393,35 @@ protected function doTestTranslations($path, array $values) {
   /**
    * Tests that the given path provides the correct alternate hreflang links.
    *
-   * @param \Drupal\Core\Url $url
-   *   The path to be tested.
+   * @param \Drupal\node\Entity\Node $node
+   *   The node to be tested.
    */
-  protected function doTestAlternateHreflangLinks(Url $url) {
+  protected function doTestAlternateHreflangLinks(Node $node) {
+    $url = $node->toUrl();
     $languages = $this->container->get('language_manager')->getLanguages();
     $url->setAbsolute();
     $urls = [];
+    $translations = [];
     foreach ($this->langcodes as $langcode) {
       $language_url = clone $url;
       $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
+      $translations[$langcode] = $node->getTranslation($langcode);
     }
     foreach ($this->langcodes as $langcode) {
-      $this->drupalGet($urls[$langcode]);
-      foreach ($urls as $alternate_langcode => $language_url) {
-        // Retrieve desired link elements from the HTML head.
-        $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]',
-          [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]);
-        $this->assert(isset($links[0]), new FormattableMarkup('The %langcode node translation has the correct alternate hreflang link for %alternate_langcode: %link.', ['%langcode' => $langcode, '%alternate_langcode' => $alternate_langcode, '%link' => $url->toString()]));
+      // Skip unpublished translations.
+      if ($translations[$langcode]->isPublished()) {
+        $this->drupalGet($urls[$langcode]);
+        foreach ($urls as $alternate_langcode => $language_url) {
+          // Retrieve desired link elements from the HTML head.
+          $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]',
+             [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]);
+          if ($translations[$alternate_langcode]->isPublished()) {
+            $this->assert(isset($links[0]), new FormattableMarkup('The %langcode node translation has the correct alternate hreflang link for %alternate_langcode: %link.', ['%langcode' => $langcode, '%alternate_langcode' => $alternate_langcode, '%link' => $url->toString()]));
+          }
+          else {
+            $this->assertFalse(isset($links[0]), new FormattableMarkup('The %langcode node translation has an hreflang link for unpublished %alternate_langcode translation: %link.', ['%langcode' => $langcode, '%alternate_langcode' => $alternate_langcode, '%link' => $url->toString()]));
+          }
+        }
       }
     }
   }