diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php
index 2a9e91999164272d26634bb88abf26e17be309d3..4e6b58164d48bfb4577eec2004422755c5def829 100644
--- a/core/.phpstan-baseline.php
+++ b/core/.phpstan-baseline.php
@@ -31992,24 +31992,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php',
 ];
-$ignoreErrors[] = [
-	// identifier: property.notFound
-	'message' => '#^Access to an undefined property Drupal\\\\path\\\\Plugin\\\\Field\\\\FieldType\\\\PathItem\\:\\:\\$alias\\.$#',
-	'count' => 3,
-	'path' => __DIR__ . '/modules/path/src/Plugin/Field/FieldType/PathItem.php',
-];
-$ignoreErrors[] = [
-	// identifier: property.notFound
-	'message' => '#^Access to an undefined property Drupal\\\\path\\\\Plugin\\\\Field\\\\FieldType\\\\PathItem\\:\\:\\$langcode\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/modules/path/src/Plugin/Field/FieldType/PathItem.php',
-];
-$ignoreErrors[] = [
-	// identifier: property.notFound
-	'message' => '#^Access to an undefined property Drupal\\\\path\\\\Plugin\\\\Field\\\\FieldType\\\\PathItem\\:\\:\\$pid\\.$#',
-	'count' => 5,
-	'path' => __DIR__ . '/modules/path/src/Plugin/Field/FieldType/PathItem.php',
-];
 $ignoreErrors[] = [
 	// identifier: return.missing
 	'message' => '#^Method Drupal\\\\path\\\\Plugin\\\\Field\\\\FieldType\\\\PathItem\\:\\:postSave\\(\\) should return bool but return statement is missing\\.$#',
diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
index c40b95ae58537005d340c5f511b09ac5a1ca6f77..1c5bdaa1220846a262c3d0760828fa635a61f095 100644
--- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
@@ -173,9 +173,12 @@ protected function getExpectedDocument(): array {
           'default_langcode' => TRUE,
           'langcode' => 'en',
           'path' => [
-            'alias' => '/llama',
-            'pid' => 1,
-            'langcode' => 'en',
+            [
+              'alias' => '/llama',
+              'pid' => 1,
+              'langcode' => 'en',
+              'variant' => 'default',
+            ],
           ],
           'promote' => TRUE,
           'revision_timestamp' => '1973-11-29T21:33:09+00:00',
@@ -294,7 +297,7 @@ public function testPatchPath(): void {
     $normalization = $this->getDocumentFromResponse($response);
 
     // Change node's path alias.
-    $normalization['data']['attributes']['path']['alias'] .= 's-rule-the-world';
+    $normalization['data']['attributes']['path'][0]['alias'] .= 's-rule-the-world';
 
     // Create node PATCH request.
     $request_options = $this->getAuthenticationRequestOptions();
diff --git a/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
index 6618515e3e59eb7e7852a05083bd3bfdb4fa30d6..ea89b45a610087d19d96a56337f338887749861c 100644
--- a/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
@@ -99,6 +99,7 @@ protected function getExpectedDocument(): array {
           'self' => ['href' => $self_url->toString()],
         ],
         'attributes' => [
+          'variant' => NULL,
           'alias' => '/frontpage1',
           'path' => '/<front>',
           'langcode' => 'en',
diff --git a/core/modules/jsonapi/tests/src/Functional/TermTest.php b/core/modules/jsonapi/tests/src/Functional/TermTest.php
index c76de970eb0c608f0990c3036433d4b7255610a6..0b3602cd86ef9868222d0595904a28dfa3a5a6a3 100644
--- a/core/modules/jsonapi/tests/src/Functional/TermTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/TermTest.php
@@ -274,9 +274,12 @@ protected function getExpectedDocument(): array {
           'langcode' => 'en',
           'name' => 'Llama',
           'path' => [
-            'alias' => '/llama',
-            'pid' => 1,
-            'langcode' => 'en',
+            [
+              'alias' => '/llama',
+              'pid' => 1,
+              'langcode' => 'en',
+              'variant' => 'default',
+            ],
           ],
           'weight' => 0,
           'drupal_internal__tid' => 1,
@@ -418,7 +421,7 @@ public function testPatchPath(): void {
     $normalization = $this->getDocumentFromResponse($response);
 
     // Change term's path alias.
-    $normalization['data']['attributes']['path']['alias'] .= 's-rule-the-world';
+    $normalization['data']['attributes']['path'][0]['alias'] .= 's-rule-the-world';
 
     // Create term PATCH request.
     $request_options[RequestOptions::BODY] = Json::encode($normalization);
diff --git a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
index a8fe843e518b8dec4f559aa826e58a086cb3abdc..fe6431f415bbf358a563d204641e8af6476264e5 100644
--- a/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
+++ b/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php
@@ -200,6 +200,7 @@ protected function getExpectedNormalizedEntity() {
           'alias' => '/llama',
           'pid' => 1,
           'langcode' => 'en',
+          'variant' => 'default',
         ],
       ],
     ];
diff --git a/core/modules/path/js/path.js b/core/modules/path/js/path.js
index 9d55741b78d9b462e8c9eec1486c52e27ca9513b..deaa074d2259387f5241a114e718690a6aa86e70 100644
--- a/core/modules/path/js/path.js
+++ b/core/modules/path/js/path.js
@@ -16,9 +16,7 @@
       $(context)
         .find('.path-form')
         .drupalSetSummary((context) => {
-          const pathElement = document.querySelector(
-            '.js-form-item-path-0-alias input',
-          );
+          const pathElement = context.querySelector('.js-form-item input');
           const path = pathElement && pathElement.value;
           return path
             ? Drupal.t('Alias: @alias', { '@alias': path })
diff --git a/core/modules/path/path.post_update.php b/core/modules/path/path.post_update.php
index 10397185598b1933de7f3c2e9eaa96ab66ac1a9d..bf2f6808fbae3ec615c19b54d385e225a947eaac 100644
--- a/core/modules/path/path.post_update.php
+++ b/core/modules/path/path.post_update.php
@@ -13,3 +13,10 @@ function path_removed_post_updates(): array {
     'path_post_update_create_language_content_settings' => '9.0.0',
   ];
 }
+
+/**
+ * No-op update.
+ */
+function path_post_update_variant_reset_container(&$sandbox = NULL): void {
+  // No-op: force a container reset since services changed.
+}
diff --git a/core/modules/path/path.services.yml b/core/modules/path/path.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d593bd02b8c0044e6de839ed0f4d230a87dec981
--- /dev/null
+++ b/core/modules/path/path.services.yml
@@ -0,0 +1,9 @@
+services:
+  _defaults:
+    autoconfigure: true
+    autowire: true
+
+  Drupal\path\PathVariant\PathVariantRepositoryInterface:
+    class: Drupal\path\PathVariant\PathVariantRepository
+  Drupal\path\EventSubscriber\EntityPathsEventSubscriber: ~
+  Drupal\path\EventSubscriber\PathVariantEventSubscriber: ~
diff --git a/core/modules/path/src/Enum/PathVariantEnumInterface.php b/core/modules/path/src/Enum/PathVariantEnumInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..6759cb20a1b77d1ec3fa4ee9b2abaf0e1b3288d2
--- /dev/null
+++ b/core/modules/path/src/Enum/PathVariantEnumInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\Enum;
+
+interface PathVariantEnumInterface extends \UnitEnum {
+
+  /**
+   * The machine name of a path variant.
+   *
+   * This is used in the PathAlias entities' `variant` field.
+   */
+  public function getMachineName(): string;
+
+}
diff --git a/core/modules/path/src/Event/EntityPathsEvent.php b/core/modules/path/src/Event/EntityPathsEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..dcca88a225a7d3d6bf4cfcdccc34b8a5a7ddba09
--- /dev/null
+++ b/core/modules/path/src/Event/EntityPathsEvent.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\Event;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\path\PathVariant\PathVariant;
+use Drupal\path\PathVariant\PlaceHolderInternalPath;
+
+/**
+ * Event for getting internal paths for an entity.
+ *
+ * @phpstan-type InternalPaths iterable<array{non-empty-string|\Drupal\path\PathVariant\PlaceHolderInternalPath, \Drupal\path\PathVariant\PathVariant}>
+ */
+final class EntityPathsEvent {
+
+  /**
+   * @var list<array{non-empty-string, \Drupal\path\PathVariant\PathVariant}>
+   */
+  private array $internalPaths = [];
+
+  /**
+   * Constructs a new EntityPathsEvent.
+   */
+  private function __construct(
+    private EntityInterface $entity,
+  ) {
+  }
+
+  /**
+   * Factory for creating a new entity paths event.
+   *
+   * @internal
+   *   Not for public use.
+   */
+  public static function create(
+    EntityInterface $entity,
+  ): static {
+    return new static($entity,
+    );
+  }
+
+  /**
+   * Get the entity.
+   *
+   * @phpstan-pure
+   */
+  public function getEntity(): EntityInterface {
+    return $this->entity;
+  }
+
+  /**
+   * Adds an internal path.
+   *
+   * @param non-empty-string|\Drupal\path\PathVariant\PlaceHolderInternalPath $path
+   *   A path. Must be prefixed by '/', or PlaceHolderInternalPath if the
+   *   internal path cannot be determined yet.
+   * @param \Drupal\path\PathVariant\PathVariant $variant
+   *   The variant.
+   *
+   * @return $this
+   *   The object itself for chaining.
+   *
+   * @throws \InvalidArgumentException
+   *   When path is invalid.
+   */
+  public function addInternalPath(PlaceHolderInternalPath|string $path, PathVariant $variant): static {
+    if (\is_string($path) && FALSE === \str_starts_with($path, '/')) {
+      throw new \InvalidArgumentException('Path must be prefixed with forward slash.');
+    }
+
+    $this->internalPaths[] = [$path, $variant];
+
+    return $this;
+  }
+
+  /**
+   * Get the internal path and variants for this entity.
+   *
+   * @return InternalPaths
+   *   Internal paths and variants.
+   *
+   * @internal
+   *    Not for public use.
+   */
+  public function getInternalPaths(): iterable {
+    return $this->internalPaths;
+  }
+
+}
diff --git a/core/modules/path/src/Event/PathVariantEvent.php b/core/modules/path/src/Event/PathVariantEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..938c76d8c9333bb3cafb934324735bd3552e1fce
--- /dev/null
+++ b/core/modules/path/src/Event/PathVariantEvent.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\Event;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\path\PathVariant\PathVariant;
+
+/**
+ * Event for collecting path variants for an entity/bundle combination.
+ *
+ * @phpstan-type Variants iterable<\Drupal\path\PathVariant\PathVariant>
+ */
+final class PathVariantEvent {
+
+  /**
+   * @var list<\Drupal\path\PathVariant\PathVariant>
+   */
+  private array $variants = [];
+
+  /**
+   * Constructs a new PathVariantEvent.
+   */
+  private function __construct(
+    private string $entityTypeId,
+    private string $bundle,
+  ) {
+  }
+
+  /**
+   * Factory for creating a new path variant event.
+   *
+   * @internal
+   *   Not for public use.
+   */
+  public static function create(
+    string $entityTypeId,
+    string $bundle,
+  ): static {
+    return new static($entityTypeId, $bundle);
+  }
+
+  /**
+   * Factory for creating a new path variant event from an entity.
+   *
+   * @internal
+   *    Not for public use.
+   */
+  public static function createFromEntity(EntityInterface $entity): static {
+    return new static($entity->getEntityTypeId(), $entity->bundle());
+  }
+
+  /**
+   * Get the entity type ID.
+   *
+   * @phpstan-pure
+   */
+  public function getEntityTypeId(): string {
+    return $this->entityTypeId;
+  }
+
+  /**
+   * Get the bundle.
+   *
+   * @phpstan-pure
+   */
+  public function getBundle(): string {
+    return $this->bundle;
+  }
+
+  /**
+   * Adds a variant.
+   *
+   * @return $this
+   *   The object itself for chaining.
+   */
+  public function addVariant(PathVariant $variant): static {
+    $this->variants[] = $variant;
+
+    return $this;
+  }
+
+  /**
+   * Get the variants for this entity type and bundle.
+   *
+   * @return Variants
+   *
+   * @internal
+   *    Not for public use.
+   */
+  public function getVariants(): iterable {
+    return $this->variants;
+  }
+
+}
diff --git a/core/modules/path/src/EventSubscriber/EntityPathsEventSubscriber.php b/core/modules/path/src/EventSubscriber/EntityPathsEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..e8236b7267c494cd43d9be8993de31e7168a570c
--- /dev/null
+++ b/core/modules/path/src/EventSubscriber/EntityPathsEventSubscriber.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\EventSubscriber;
+
+use Drupal\path\Event\EntityPathsEvent;
+use Drupal\path\PathVariant\PathVariant;
+use Drupal\path\PathVariant\PlaceHolderInternalPath;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Responds with internal paths provided by core.
+ */
+final class EntityPathsEventSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Subscriber for canonical internal path.
+   */
+  public function canonicalInternalPath(EntityPathsEvent $event): void {
+    $event->addInternalPath(
+      $event->getEntity()->isNew() ? PlaceHolderInternalPath::create() : '/' . $event->getEntity()->toUrl(rel: 'canonical')->getInternalPath(),
+      PathVariant::createDefault(),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      EntityPathsEvent::class => [
+        ['canonicalInternalPath'],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/path/src/EventSubscriber/PathVariantEventSubscriber.php b/core/modules/path/src/EventSubscriber/PathVariantEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..55ff1ef00d23790d93111697ef7dc8e28cdf3b86
--- /dev/null
+++ b/core/modules/path/src/EventSubscriber/PathVariantEventSubscriber.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\EventSubscriber;
+
+use Drupal\path\Event\PathVariantEvent;
+use Drupal\path\PathVariant\PathVariant;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Responds with variants provided by core.
+ */
+final class PathVariantEventSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The default variant.
+   */
+  public function defaultVariant(PathVariantEvent $event): void {
+    $event->addVariant(PathVariant::createDefault());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      PathVariantEvent::class => 'defaultVariant',
+    ];
+  }
+
+}
diff --git a/core/modules/path/src/Hook/PathHooks.php b/core/modules/path/src/Hook/PathHooks.php
index 42b79e270c45315d74a371bb2066f4ae52a4e469..b3934780f8954db6583a4801ae2a4cc8943df793 100644
--- a/core/modules/path/src/Hook/PathHooks.php
+++ b/core/modules/path/src/Hook/PathHooks.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\path\Hook;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
@@ -102,7 +103,13 @@ public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_t
   #[Hook('entity_base_field_info')]
   public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
     if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
-      $fields['path'] = BaseFieldDefinition::create('path')->setLabel(t('URL alias'))->setTranslatable(TRUE)->setDisplayOptions('form', ['type' => 'path', 'weight' => 30])->setDisplayConfigurable('form', TRUE)->setComputed(TRUE);
+      $fields['path'] = BaseFieldDefinition::create('path')
+        ->setLabel(t('URL alias'))
+        ->setTranslatable(TRUE)
+        ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
+        ->setDisplayOptions('form', ['type' => 'path', 'weight' => 30])
+        ->setDisplayConfigurable('form', TRUE)
+        ->setComputed(TRUE);
       return $fields;
     }
   }
diff --git a/core/modules/path/src/PathVariant/CorePathVariants.php b/core/modules/path/src/PathVariant/CorePathVariants.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb636b10c6689edb9a578f23dda4783ac7c0470e
--- /dev/null
+++ b/core/modules/path/src/PathVariant/CorePathVariants.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\PathVariant;
+
+use Drupal\path\Enum\PathVariantEnumInterface;
+
+/**
+ * Represents static path variants provided by core.
+ */
+enum CorePathVariants implements PathVariantEnumInterface {
+
+  case Default;
+
+  public function getMachineName(): string {
+    return match ($this) {
+      static::Default => PathVariant::DEFAULT,
+    };
+  }
+
+}
diff --git a/core/modules/path/src/PathVariant/PathVariant.php b/core/modules/path/src/PathVariant/PathVariant.php
new file mode 100644
index 0000000000000000000000000000000000000000..933bd2f42057ca644caf7a7bf2c82086a8644fc6
--- /dev/null
+++ b/core/modules/path/src/PathVariant/PathVariant.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\PathVariant;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\path\Enum\PathVariantEnumInterface;
+
+/**
+ * Represents a path variant for a bundle.
+ */
+final class PathVariant {
+
+  /**
+   * Represents the default variant.
+   */
+  public const DEFAULT = 'default';
+
+  private function __construct(
+    private PathVariantEnumInterface|string $variant,
+    private \Stringable|string $label,
+  ) {
+  }
+
+  /**
+   * Represents a path variant for a bundle.
+   */
+  public static function create(
+    PathVariantEnumInterface|string $variant,
+    \Stringable|string $label,
+  ): static {
+    return new static($variant, $label);
+  }
+
+  /**
+   * Create a variant representing the default for an entity.
+   *
+   * @internal
+   *   Not for public use.
+   */
+  public static function createDefault(): static {
+    return new static(CorePathVariants::Default, new TranslatableMarkup('Default'));
+  }
+
+  /**
+   * The variant, either a string or enum.
+   *
+   * Implementations of static variants should use enums, variants backed by
+   * user configuration may choose to use string.
+   *
+   * @return \Drupal\path\Enum\PathVariantEnumInterface|string
+   */
+  public function getVariant(): PathVariantEnumInterface|string {
+    return $this->variant;
+  }
+
+  /**
+   * Convert the variant to a string.
+   *
+   * Used as the value of the PathAlias variant field.
+   */
+  public function getVariantStringed(): string {
+    return $this->variant instanceof PathVariantEnumInterface ? $this->variant->getMachineName() : $this->variant;
+  }
+
+  /**
+   * Get the user-facing label for this variant.
+   */
+  public function getLabel(): \Stringable|string {
+    return $this->label;
+  }
+
+  /**
+   * Converts the variant to a string.
+   *
+   * Primarily used as the value of the PathAlias variant field and
+   * serialization.
+   */
+  public function __toString(): string {
+    return $this->getVariantStringed();
+  }
+
+}
diff --git a/core/modules/path/src/PathVariant/PathVariantRepository.php b/core/modules/path/src/PathVariant/PathVariantRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..a10b1c63a1f45451c37e79717a3b0af8d6f209e1
--- /dev/null
+++ b/core/modules/path/src/PathVariant/PathVariantRepository.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\PathVariant;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\path\Event\EntityPathsEvent;
+use Drupal\path\Event\PathVariantEvent;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Constructs the path variant repository.
+ */
+final class PathVariantRepository implements PathVariantRepositoryInterface {
+
+  /**
+   * Constructs a PathVariantRepository.
+   */
+  public function __construct(
+    public EventDispatcherInterface $dispatcher,
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInternalPaths(EntityInterface $entity): iterable {
+    $this->dispatcher->dispatch($event = EntityPathsEvent::create($entity));
+    return $event->getInternalPaths();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInternalPathByPathVariant(EntityInterface $entity, PathVariant $pathVariant): string|PlaceHolderInternalPath {
+    foreach ($this->getInternalPaths($entity) as [$internalPath, $variant]) {
+      if ($variant->getVariant() === $pathVariant->getVariant()) {
+        return $internalPath;
+      }
+    }
+
+    throw new \InvalidArgumentException('Unknown path variant.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultPathVariant(EntityInterface $entity): PathVariant {
+    foreach ($this->getInternalPaths($entity) as [1 => $variant]) {
+      // Return the first.
+      // @todo this can be improved in a future issue.
+      return $variant;
+    }
+
+    throw new \InvalidArgumentException('Entity has no path variants.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPathVariantsForEntity(EntityInterface $entity): iterable {
+    $this->dispatcher->dispatch($event = PathVariantEvent::createFromEntity($entity));
+    return $event->getVariants();
+  }
+
+}
diff --git a/core/modules/path/src/PathVariant/PathVariantRepositoryInterface.php b/core/modules/path/src/PathVariant/PathVariantRepositoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..07b71ece4dc21dd907c4d22ff8f963dd3cd72901
--- /dev/null
+++ b/core/modules/path/src/PathVariant/PathVariantRepositoryInterface.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\PathVariant;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Interface for path variant repository.
+ *
+ * @phpstan-import-type InternalPaths from \Drupal\path\Event\EntityPathsEvent
+ * @phpstan-import-type Variants from \Drupal\path\Event\PathVariantEvent
+ */
+interface PathVariantRepositoryInterface {
+
+  /**
+   * Get internal paths for an entity.
+   *
+   * When final internal paths cannot be computed, such as before an entity is
+   * saved for the first time, a placeholder value object is used.
+   *
+   * @return InternalPaths
+   */
+  public function getInternalPaths(EntityInterface $entity): iterable;
+
+  /**
+   * Get an internal path for the path variant of an entity.
+   *
+   * @return non-empty-string|\Drupal\path\PathVariant\PlaceHolderInternalPath
+   *   The internal path.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the path variant does not exist.
+   */
+  public function getInternalPathByPathVariant(EntityInterface $entity, PathVariant $pathVariant): string|PlaceHolderInternalPath;
+
+  /**
+   * Get the default path variant.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the path variant does not exist.
+   */
+  public function getDefaultPathVariant(EntityInterface $entity): PathVariant;
+
+  /**
+   * Get the path variants for an entity.
+   *
+   * @return Variants
+   */
+  public function getPathVariantsForEntity(EntityInterface $entity): iterable;
+
+}
diff --git a/core/modules/path/src/PathVariant/PlaceHolderInternalPath.php b/core/modules/path/src/PathVariant/PlaceHolderInternalPath.php
new file mode 100644
index 0000000000000000000000000000000000000000..362f0a506a47749f971626b322f4d032f3144fbc
--- /dev/null
+++ b/core/modules/path/src/PathVariant/PlaceHolderInternalPath.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path\PathVariant;
+
+/**
+ * Represents an internal path that cannot be determined yet.
+ *
+ * Usually since an entity hasn't been saved yet.
+ */
+final class PlaceHolderInternalPath {
+
+  private function __construct() {}
+
+  /**
+   * Creates a new internal path placeholder.
+   *
+   * @internal
+   *   Not for public use.
+   */
+  public static function create(): static {
+    return new static();
+  }
+
+}
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
index 08710f943375eb5c852a0539b4a3eb2636095a84..e6c1178ee0298c00c610b86484a6430f008ec78d 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
@@ -1,14 +1,21 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\path\Plugin\Field\FieldType;
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Field\FieldItemList;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\ComputedItemListTrait;
+use Drupal\path\PathVariant\CorePathVariants;
+use Drupal\path\PathVariant\PathVariantRepositoryInterface;
+use Drupal\path\PathVariant\PlaceHolderInternalPath;
+use Drupal\path_alias\AliasRepositoryInterface;
+use Drupal\path_alias\PathAliasStorage;
 
 /**
- * Represents a configurable entity path field.
+ * Represents a path which may be overridden.
  */
 class PathFieldItemList extends FieldItemList {
 
@@ -18,27 +25,37 @@ class PathFieldItemList extends FieldItemList {
    * {@inheritdoc}
    */
   protected function computeValue() {
-    // Default the langcode to the current language if this is a new entity or
-    // there is no alias for an existent entity.
-    // @todo Set the langcode to not specified for untranslatable fields
-    //   in https://www.drupal.org/node/2689459.
-    $value = ['langcode' => $this->getLangcode()];
-
     $entity = $this->getEntity();
-    if (!$entity->isNew()) {
-      /** @var \Drupal\path_alias\AliasRepositoryInterface $path_alias_repository */
-      $path_alias_repository = \Drupal::service('path_alias.repository');
+    $delta = 0;
+    foreach (static::pathVariantRepository()->getInternalPaths($entity) as [$internalPath, $variant]) {
+      $langCode = $this->getLangcode();
+      $pathAlias = $internalPath instanceof PlaceHolderInternalPath
+        ? NULL
+        : static::pathAliasRepository()->lookupBySystemPath(
+          $internalPath,
+          $langCode,
+          // The 'full' view mode gets the default/legacy NULL value.
+          $variant->getVariant() === CorePathVariants::Default ? NULL : (string) $variant,
+        );
 
-      if ($path_alias = $path_alias_repository->lookupBySystemPath('/' . $entity->toUrl()->getInternalPath(), $this->getLangcode())) {
-        $value = [
-          'alias' => $path_alias['alias'],
-          'pid' => $path_alias['id'],
-          'langcode' => $path_alias['langcode'],
-        ];
-      }
+      $value = [
+        // Default the langcode to the current language if this is a new
+        // entity or there is no alias for an existent entity.
+        // @todo Set the langcode to not specified for untranslatable fields
+        // in https://www.drupal.org/node/2689459.
+        'langcode' => $langCode,
+        'variant' => $variant,
+      ];
+      $this->list[$delta] = $this->createItem(
+        $delta,
+        ($pathAlias !== NULL ? [
+          'alias' => $pathAlias['alias'],
+          'pid' => $pathAlias['id'],
+          'langcode' => $pathAlias['langcode'],
+        ] : []) + $value,
+      );
+      $delta++;
     }
-
-    $this->list[0] = $this->createItem(0, $value);
   }
 
   /**
@@ -57,12 +74,39 @@ public function defaultAccess($operation = 'view', ?AccountInterface $account =
   public function delete() {
     // Delete all aliases associated with this entity in the current language.
     $entity = $this->getEntity();
-    $path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
-    $entities = $path_alias_storage->loadByProperties([
-      'path' => '/' . $entity->toUrl()->getInternalPath(),
-      'langcode' => $entity->language()->getId(),
-    ]);
-    $path_alias_storage->delete($entities);
+    /** @var \Drupal\path_alias\PathAliasInterface[] $pathAliases */
+    $pathAliases = [];
+    foreach (static::pathVariantRepository()->getInternalPaths($entity) as [$internalPath]) {
+      $pathAliases += static::pathAliasStorage()->loadByProperties([
+        'path' => $internalPath,
+        'langcode' => $entity->language()->getId(),
+      ]);
+    }
+    static::pathAliasStorage()->delete($pathAliases);
+  }
+
+  /**
+   * Path alias storage.
+   */
+  private static function pathAliasStorage(): PathAliasStorage {
+    /** @var \Drupal\path_alias\PathAliasStorage */
+    return \Drupal::entityTypeManager()->getStorage('path_alias');
+  }
+
+  /**
+   * Path alias repository service.
+   */
+  private static function pathAliasRepository(): AliasRepositoryInterface {
+    /** @var \Drupal\path_alias\AliasRepositoryInterface */
+    return \Drupal::service(AliasRepositoryInterface::class);
+  }
+
+  /**
+   * Get path variant repository service.
+   */
+  private static function pathVariantRepository(): PathVariantRepositoryInterface {
+    /** @var \Drupal\path\PathVariant\PathVariantRepositoryInterface */
+    return \Drupal::service(PathVariantRepositoryInterface::class);
   }
 
 }
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
index 6dd9111b225d1902996ccba046fe36b6296962f1..8befa982174c7a3acc933155d500976942269282 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\path\Plugin\Field\FieldType;
 
 use Drupal\Component\Utility\Random;
@@ -9,9 +11,17 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\TypedData\DataDefinition;
+use Drupal\path\PathVariant\CorePathVariants;
+use Drupal\path\PathVariant\PathVariantRepositoryInterface;
+use Drupal\path_alias\PathAliasStorage;
 
 /**
- * Defines the 'path' entity field type.
+ * Defines the 'path' field type.
+ *
+ * @property string|null $alias
+ * @property int|null $pid
+ * @property string|null $langcode
+ * @property \Drupal\path\PathVariant\PathVariant|null $variant
  */
 #[FieldType(
   id: "path",
@@ -34,6 +44,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
       ->setLabel(t('Path id'));
     $properties['langcode'] = DataDefinition::create('string')
       ->setLabel(t('Language Code'));
+    $properties['variant'] = DataDefinition::create('any')
+      ->setLabel(t('Path variant'));
     return $properties;
   }
 
@@ -64,43 +76,39 @@ public function preSave() {
    * {@inheritdoc}
    */
   public function postSave($update) {
-    $path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
-    $entity = $this->getEntity();
-
-    // If specified, rely on the langcode property for the language, so that the
-    // existing language of an alias can be kept. That could for example be
-    // unspecified even if the field/entity has a specific langcode.
-    $alias_langcode = ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode();
-
-    // If we have an alias, we need to create or update a path alias entity.
-    if ($this->alias) {
-      if (!$update || !$this->pid) {
-        $path_alias = $path_alias_storage->create([
-          'path' => '/' . $entity->toUrl()->getInternalPath(),
+    // If we have an alias, create or update a path alias entity.
+    if (is_string($this->alias) && strlen($this->alias) > 0) {
+      // When this is an entity insert or there is no existing path alias ID:
+      if (!$update || $this->pid === NULL) {
+        $variant = $this->variant ?? static::pathVariantRepository()->getDefaultPathVariant($this->getEntity());
+
+        $path_alias = static::pathAliasStorage()->create([
+          'path' => static::pathVariantRepository()->getInternalPathByPathVariant($this->getEntity(), $variant),
           'alias' => $this->alias,
-          'langcode' => $alias_langcode,
+          'variant' => $variant->getVariant() === CorePathVariants::Default ? NULL : (string) $variant,
+          // If specified, rely on the langcode property for the language, so that the
+          // existing language of an alias can be kept. That could for example be
+          // unspecified even if the field/entity has a specific langcode.
+          'langcode' => ($this->langcode && $this->pid) ? $this->langcode : $this->getLangcode(),
         ]);
         $path_alias->save();
-        $this->pid = $path_alias->id();
+        $this->pid = (int) $path_alias->id();
       }
-      elseif ($this->pid) {
-        $path_alias = $path_alias_storage->load($this->pid);
+      // When this is an entity update and there is an existing path alias ID:
+      elseif ($this->pid !== NULL) {
+        $path_alias = static::pathAliasStorage()->load($this->pid);
 
-        if ($this->alias != $path_alias->getAlias()) {
-          $path_alias->setAlias($this->alias);
-          $path_alias->save();
+        if ($this->alias !== $path_alias->getAlias()) {
+          $path_alias->setAlias($this->alias)->save();
         }
       }
     }
-    elseif ($this->pid && !$this->alias) {
+    elseif ($this->pid !== NULL) {
       // Otherwise, delete the old alias if the user erased it.
-      $path_alias = $path_alias_storage->load($this->pid);
-      if ($entity->isDefaultRevision()) {
-        $path_alias_storage->delete([$path_alias]);
-      }
-      else {
-        $path_alias_storage->deleteRevision($path_alias->getRevisionID());
-      }
+      $path_alias = static::pathAliasStorage()->load($this->pid);
+      $this->getEntity()->isDefaultRevision()
+        ? static::pathAliasStorage()->delete([$path_alias])
+        : static::pathAliasStorage()->deleteRevision($path_alias->getRevisionId());
     }
   }
 
@@ -120,4 +128,20 @@ public static function mainPropertyName() {
     return 'alias';
   }
 
+  /**
+   * Path alias storage.
+   */
+  private static function pathAliasStorage(): PathAliasStorage {
+    /** @var \Drupal\path_alias\PathAliasStorage */
+    return \Drupal::entityTypeManager()->getStorage('path_alias');
+  }
+
+  /**
+   * Get path variant repository service.
+   */
+  private static function pathVariantRepository(): PathVariantRepositoryInterface {
+    /** @var \Drupal\path\PathVariant\PathVariantRepositoryInterface */
+    return \Drupal::service(PathVariantRepositoryInterface::class);
+  }
+
 }
diff --git a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
index 837929520278c034117ba4bcc13c228b1a4a7fb4..8e691bbb8a866a210db70735c0f4962a55d2fb0a 100644
--- a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
+++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
@@ -7,6 +7,9 @@
 use Drupal\Core\Field\WidgetBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\path\PathVariant\PathVariantRepositoryInterface;
+use Drupal\path\Plugin\Field\FieldType\PathItem;
+use Drupal\path_alias\PathAliasStorage;
 use Symfony\Component\Validator\ConstraintViolationInterface;
 
 /**
@@ -25,40 +28,39 @@ class PathWidget extends WidgetBase {
   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
     $entity = $items->getEntity();
 
-    $element += [
-      '#element_validate' => [[static::class, 'validateFormElement']],
-    ];
+    $item = $items[$delta];
+    assert($item instanceof PathItem);
+
+    $element += ['#element_validate' => [[static::class, 'validateFormElement']]];
+
     $element['alias'] = [
       '#type' => 'textfield',
-      '#title' => $element['#title'],
-      '#default_value' => $items[$delta]->alias,
+      '#title' => $this->fieldDefinition->getLabel(),
+      '#default_value' => $item->alias,
       '#required' => $element['#required'],
       '#maxlength' => 255,
       '#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.'),
     ];
-    $element['pid'] = [
-      '#type' => 'value',
-      '#value' => $items[$delta]->pid,
-    ];
+
+    // Pass through item values as hidden values:
+    $element['langcode'] = ['#type' => 'value', '#value' => $item->langcode];
+    $element['pid'] = ['#type' => 'value', '#value' => $item->pid];
     $element['source'] = [
       '#type' => 'value',
-      '#value' => !$entity->isNew() ? '/' . $entity->toUrl()->getInternalPath() : NULL,
-    ];
-    $element['langcode'] = [
-      '#type' => 'value',
-      '#value' => $items[$delta]->langcode,
+      '#value' => $entity->isNew() ? NULL : static::pathVariantRepository()->getInternalPathByPathVariant($entity, $item->variant),
     ];
+    $element['variant'] = ['#type' => 'value', '#value' => $item->variant];
 
     // If the advanced settings tabs-set is available (normally rendered in the
     // second column on wide-resolutions), place the field as a details element
     // in this tab-set.
-    if (isset($form['advanced'])) {
+    if (array_key_exists('advanced', $form)) {
       $element += [
         '#type' => 'details',
         '#title' => $this->t('URL path settings'),
-        '#open' => !empty($items[$delta]->alias),
+        '#open' => $item->alias !== NULL,
         '#group' => 'advanced',
-        '#access' => $entity->get('path')->access('edit'),
+        '#access' => $entity->get($items->getName())->access('edit'),
         '#attributes' => [
           'class' => ['path-form'],
         ],
@@ -66,12 +68,36 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
           'library' => ['path/drupal.path'],
         ],
       ];
-      $element['#weight'] = 30;
+      $element['#weight'] = (float) sprintf('30.%s', $delta);
     }
 
     return $element;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state): array {
+    $elements = [];
+
+    foreach ($items as $delta => $item) {
+      $titleArgs = [
+        '@title' => $this->fieldDefinition->getLabel(),
+        '@variant' => $item->variant?->getLabel() ?? $this->t('Unknown'),
+      ];
+
+      $element = [
+        '#title' => $items->count() === 1
+          ? $this->t('@title', $titleArgs, ['context' => 'entity path no variants'])
+          // Display variant label when there is at least one available.
+          : $this->t('@title (@variant)', $titleArgs, ['context' => 'entity path variants']),
+      ];
+      $elements[$delta] = $this->formSingleElement($items, $delta, $element, $form, $form_state);
+    }
+
+    return $elements;
+  }
+
   /**
    * Form element validation handler for URL alias form element.
    *
@@ -87,14 +113,13 @@ public static function validateFormElement(array &$element, FormStateInterface $
       $form_state->setValueForElement($element['alias'], $alias);
 
       /** @var \Drupal\path_alias\PathAliasInterface $path_alias */
-      $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
+      $path_alias = static::pathAliasStorage()->create([
         'path' => $element['source']['#value'],
         'alias' => $alias,
         'langcode' => $element['langcode']['#value'],
       ]);
-      $violations = $path_alias->validate();
 
-      foreach ($violations as $violation) {
+      foreach ($path_alias->validate() as $violation) {
         // Newly created entities do not have a system path yet, so we need to
         // disregard some violations.
         if (!$path_alias->getPath() && $violation->getPropertyPath() === 'path') {
@@ -112,4 +137,20 @@ public function errorElement(array $element, ConstraintViolationInterface $viola
     return $element['alias'];
   }
 
+  /**
+   * Get path variant repository service.
+   */
+  private static function pathVariantRepository(): PathVariantRepositoryInterface {
+    /** @var \Drupal\path\PathVariant\PathVariantRepositoryInterface */
+    return \Drupal::service(PathVariantRepositoryInterface::class);
+  }
+
+  /**
+   * Path alias storage.
+   */
+  private static function pathAliasStorage(): PathAliasStorage {
+    /** @var \Drupal\path_alias\PathAliasStorage */
+    return \Drupal::entityTypeManager()->getStorage('path_alias');
+  }
+
 }
diff --git a/core/modules/path_alias/path_alias.install b/core/modules/path_alias/path_alias.install
new file mode 100644
index 0000000000000000000000000000000000000000..47e9615965b5941fbb24af5517f57f5230dabc1c
--- /dev/null
+++ b/core/modules/path_alias/path_alias.install
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Update functions for the path module.
+ */
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Update the path_alias_revision indices.
+ */
+function path_alias_update_11001(array &$sandbox): TranslatableMarkup {
+  /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $update_manager */
+  $update_manager = \Drupal::service('entity.definition_update_manager');
+  $entity_type = $update_manager->getEntityType('path_alias');
+  $update_manager->updateEntityType($entity_type);
+
+  return new TranslatableMarkup('Updated {path_alias_revision} indices.');
+}
+
+/**
+ * Installs the `variant` field on `path_alias` entity type.
+ */
+function path_alias_update_11002(array &$sandbox): TranslatableMarkup {
+  $field = BaseFieldDefinition::create('string')
+    ->setLabel(new TranslatableMarkup('Variant'))
+    ->setDescription(new TranslatableMarkup('An alternative alias used with this path.'))
+    ->setRequired(FALSE)
+    ->setDefaultValue(NULL)
+    ->setRevisionable(TRUE);
+
+  $entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
+  $entityDefinitionUpdateManager->installFieldStorageDefinition('variant', 'path_alias', 'path', $field);
+
+  return new TranslatableMarkup('Installed the `variant` field on `path_alias` entity type.');
+}
diff --git a/core/modules/path_alias/path_alias.post_update.php b/core/modules/path_alias/path_alias.post_update.php
index 91399093cb2159d8c88ddbef7610a1bdd1ea462d..4f29b91b8dac7052c76c4ca45f14ffa44f9b4c4e 100644
--- a/core/modules/path_alias/path_alias.post_update.php
+++ b/core/modules/path_alias/path_alias.post_update.php
@@ -13,13 +13,3 @@ function path_alias_removed_post_updates(): array {
     'path_alias_post_update_drop_path_alias_status_index' => '11.0.0',
   ];
 }
-
-/**
- * Update the path_alias_revision indices.
- */
-function path_alias_post_update_update_path_alias_revision_indexes(): void {
-  /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $update_manager */
-  $update_manager = \Drupal::service('entity.definition_update_manager');
-  $entity_type = $update_manager->getEntityType('path_alias');
-  $update_manager->updateEntityType($entity_type);
-}
diff --git a/core/modules/path_alias/src/AliasRepository.php b/core/modules/path_alias/src/AliasRepository.php
index 21eb3daef0d65bc2a85a14fd9285cea81c827c29..19688b831bfec999f832489610f06266421a379e 100644
--- a/core/modules/path_alias/src/AliasRepository.php
+++ b/core/modules/path_alias/src/AliasRepository.php
@@ -65,7 +65,7 @@ public function preloadPathAlias($preloaded, $langcode) {
   /**
    * {@inheritdoc}
    */
-  public function lookupBySystemPath($path, $langcode) {
+  public function lookupBySystemPath(string $path, string $langcode, ?string $variant = NULL) {
     // See the queries above. Use LIKE for case-insensitive matching.
     $select = $this->getBaseQuery()
       ->fields('base_table', ['id', 'path', 'alias', 'langcode'])
@@ -75,7 +75,17 @@ public function lookupBySystemPath($path, $langcode) {
 
     $select->orderBy('base_table.id', 'DESC');
 
-    return $select->execute()->fetchAssoc() ?: NULL;
+    $variant === NULL
+      ? $select->condition('base_table.variant', operator: 'IS NULL')
+      : $select->condition('base_table.variant', value: $variant);
+
+    $result = $select->execute()->fetchAssoc();
+    if ($result === FALSE) {
+      return NULL;
+    }
+
+    $result['id'] = (int) $result['id'];
+    return $result;
   }
 
   /**
diff --git a/core/modules/path_alias/src/AliasRepositoryInterface.php b/core/modules/path_alias/src/AliasRepositoryInterface.php
index 432ec1d6ebe877095ff342bdf9044646e6285550..b45e577b1f33e921b1c730c29b320cf50ae6767f 100644
--- a/core/modules/path_alias/src/AliasRepositoryInterface.php
+++ b/core/modules/path_alias/src/AliasRepositoryInterface.php
@@ -42,12 +42,12 @@ public function preloadPathAlias($preloaded, $langcode);
    * @param string $langcode
    *   Language code to search the path with. If there's no path defined for
    *   that language it will search paths without language.
+   * @param string|null $variant
+   *   The path variant to get.
    *
-   * @return array|null
-   *   An array containing the 'id', 'path', 'alias' and 'langcode' properties
-   *   of a path alias, or NULL if none was found.
+   * @phpstan-return array{id: positive-int, alias: string, alias: string, langcode: string}|null
    */
-  public function lookupBySystemPath($path, $langcode);
+  public function lookupBySystemPath(string $path, string $langcode, ?string $variant = NULL);
 
   /**
    * Searches a path alias for a given alias.
diff --git a/core/modules/path_alias/src/Entity/PathAlias.php b/core/modules/path_alias/src/Entity/PathAlias.php
index 6ace9f070708e494a0c468e0aab598da586f59b6..730fbfb5400b8e248403e195a6f8f4dd24f8d776 100644
--- a/core/modules/path_alias/src/Entity/PathAlias.php
+++ b/core/modules/path_alias/src/Entity/PathAlias.php
@@ -81,6 +81,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
         ],
       ]);
 
+    $fields['variant'] = BaseFieldDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Variant'))
+      ->setDescription(new TranslatableMarkup('An alternative alias used with this path.'))
+      ->setRequired(FALSE)
+      ->setDefaultValue(NULL)
+      ->setRevisionable(TRUE);
+
     $fields['langcode']->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED);
 
     // Add the published field.
diff --git a/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php b/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
index 898be2250f7fbe8f3bb8209c0df351f1fbb9b5af..6c28ab9e73b65ab0fbac4690097e99fbb9b6557d 100644
--- a/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
+++ b/core/modules/path_alias/tests/src/Functional/Rest/PathAliasResourceTestBase.php
@@ -97,6 +97,7 @@ protected function getExpectedNormalizedEntity() {
           'value' => $this->entity->uuid(),
         ],
       ],
+      'variant' => [],
     ];
   }
 
diff --git a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
index be4aab797bfd23d74bffc12d4ad950cb6b60348e..54cefcca5cf2793772b3f603b08961fe6ddce2f1 100644
--- a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
+++ b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php
@@ -217,6 +217,7 @@ protected function getExpectedNormalizedEntity() {
           'alias' => '/llama',
           'pid' => 1,
           'langcode' => 'en',
+          'variant' => 'default',
         ],
       ],
       'status' => [