diff --git a/core/modules/link/src/Plugin/DataType/LinkResolvableUriComputed.php b/core/modules/link/src/Plugin/DataType/LinkResolvableUriComputed.php
new file mode 100644
index 00000000..968f8cd1
--- /dev/null
+++ b/core/modules/link/src/Plugin/DataType/LinkResolvableUriComputed.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\link\Plugin\DataType;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\Plugin\DataType\Uri;
+use Drupal\Core\Url;
+
+/**
+ * Provides a computed typed data value for a link that resolves to a URL string.
+ *
+ * This data type computes the rendered URL string for the parent link item and
+ * exposes cacheability metadata from the generated URL.
+ */
+#[DataType(
+  id: 'resolvable_uri',
+  label: new TranslatableMarkup('Link Resolvable URI'),
+)]
+class LinkResolvableUriComputed extends Uri implements CacheableDependencyInterface {
+
+  /**
+   * The generated URL.
+   *
+   * @var \Drupal\Core\GeneratedUrl|null
+   */
+  protected $processed = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    if ($this->processed !== NULL) {
+      return $this->processed->getGeneratedUrl();
+    }
+
+    /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $item */
+    $item = $this->getParent();
+    $this->processed = $item->getUrl()->toString(TRUE);
+
+    return $this->processed->getGeneratedUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value, $notify = TRUE): void {
+    if (!empty($value)) {
+      $parsed = UrlHelper::parse($value);
+      // If the path is not an external URL then add 'internal:' prefix to make
+      // it a valid uri.
+      if (strpos($parsed['path'], ':') === FALSE) {
+        $parsed['path'] = 'internal:' . $parsed['path'];
+      }
+      $url = Url::fromUri($parsed['path'], [
+        'query' => $parsed['query'],
+        'fragment' => $parsed['fragment'],
+      ]);
+      $this->processed = $url->toString(TRUE);
+    }
+    else {
+      $this->processed = NULL;
+    }
+
+    // Notify the parent of any changes.
+    if ($notify && isset($this->parent)) {
+      $this->parent->onChange($this->name);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    $this->getValue();
+    return $this->processed->getCacheTags();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $this->getValue();
+    return $this->processed->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    $this->getValue();
+    return $this->processed->getCacheMaxAge();
+  }
+
+}
diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
index 744124d3..3ac09dfe 100644
--- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
+++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
@@ -3,6 +3,7 @@
 namespace Drupal\link\Plugin\Field\FieldType;
 
 use Drupal\Component\Utility\Random;
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Field\Attribute\FieldType;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemBase;
@@ -38,9 +39,9 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
    */
   public static function defaultFieldSettings() {
     return [
-      'title' => LinkTitleVisibility::Optional->value,
-      'link_type' => LinkItemInterface::LINK_GENERIC,
-    ] + parent::defaultFieldSettings();
+        'title' => LinkTitleVisibility::Optional->value,
+        'link_type' => LinkItemInterface::LINK_GENERIC,
+      ] + parent::defaultFieldSettings();
   }
 
   /**
@@ -50,6 +51,20 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
     $properties['uri'] = DataDefinition::create('uri')
       ->setLabel(new TranslatableMarkup('URI'));
 
+    $properties['resolvable_uri'] = DataDefinition::create('resolvable_uri')
+      ->setLabel(t('Resolvable URI'))
+      ->setDescription(t('The processed URL for this link suitable for using in anchor href attributes.'))
+      ->setComputed(TRUE)
+      ->setInternal(FALSE)
+      ->setReadOnly(TRUE);
+
+    $properties['full_url'] = DataDefinition::create('resolvable_uri')
+      ->setLabel(t('Full URL'))
+      ->setDescription(t('The processed URL for this link suitable for using in anchor href attributes.'))
+      ->setComputed(TRUE)
+      ->setInternal(FALSE)
+      ->setReadOnly(TRUE);
+
     $properties['title'] = DataDefinition::create('string')
       ->setLabel(new TranslatableMarkup('Link text'));
 
@@ -171,6 +186,35 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
     return Url::fromUri($this->uri, (array) $this->options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($property_name, $notify = TRUE): void {
+    // Make sure that the link item values can be kept in sync with computed
+    // properties resolvable_uri and full_url.
+    if ($property_name === 'resolvable_uri' || $property_name === 'full_url') {
+      $property = $this->get($property_name);
+      if ($url = $property->getValue()) {
+        $parsed = UrlHelper::parse($url);
+        // If the path is not an external URL then add 'internal:' prefix to
+        // make it a valid uri.
+        if (strpos($parsed['path'], ':') === FALSE) {
+          $parsed['path'] = 'internal:' . $parsed['path'];
+        }
+        $this->writePropertyValue('uri', $parsed['path']);
+        // Only set the options if we have query parameters or a fragment.
+        if (!empty($parsed['query']) || !empty($parsed['fragment'])) {
+          $this->writePropertyValue('options', [
+            'query' => $parsed['query'],
+            'fragment' => $parsed['fragment'],
+          ]);
+        }
+      }
+    }
+
+    parent::onChange($property_name, $notify);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -193,6 +237,16 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
       ];
     }
     parent::setValue($values, $notify);
+
+    // Support setting the field item with only resolvable_uri or full_url
+    // property, but make sure values stay in sync if only one computed property
+    // is passed. NULL is a valid value, so we use array_key_exists().
+    if (is_array($values) && array_key_exists('resolvable_uri', $values) && !array_key_exists('uri', $values)) {
+      $this->onChange('resolvable_uri', FALSE);
+    }
+    if (is_array($values) && array_key_exists('full_url', $values) && !array_key_exists('uri', $values)) {
+      $this->onChange('full_url', FALSE);
+    }
   }
 
 }
