diff --git a/core/lib/Drupal/Core/Entity/EditorialContentEntityBase.php b/core/lib/Drupal/Core/Entity/EditorialContentEntityBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff6abcd0d552b2970e781db6faeddc50975d3606
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EditorialContentEntityBase.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Provides a base entity class with extended revision and publishing support.
+ *
+ * @ingroup entity_api
+ */
+abstract class EditorialContentEntityBase extends ContentEntityBase implements EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
+
+  use EntityChangedTrait;
+  use EntityPublishedTrait;
+  use RevisionLogEntityTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    // Add the revision metadata fields.
+    $fields += static::revisionLogBaseFieldDefinitions($entity_type);
+
+    // Add the published field.
+    $fields += static::publishedBaseFieldDefinitions($entity_type);
+
+    return $fields;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index f9f4271e3b6acc59038824e76f43115a62736c4f..5387c7ef7e3fb7c957f3ff40f3f6d8bd0bd42b43 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -271,6 +271,11 @@
  *   either \Drupal\Core\Config\Entity\ConfigEntityBase or
  *   \Drupal\Core\Entity\ContentEntityBase, with annotation for
  *   \@ConfigEntityType or \@ContentEntityType in its documentation block.
+ *   If you are defining a content entity type, it is recommended to extend the
+ *   \Drupal\Core\Entity\EditorialContentEntityBase base class in order to get
+ *   out-of-the-box support for Entity API's revisioning and publishing
+ *   features, which will allow your entity type to be used with Drupal's
+ *   editorial workflow provided by the Content Moderation module.
  * - The 'id' annotation gives the entity type ID, and the 'label' annotation
  *   gives the human-readable name of the entity type. If you are defining a
  *   content entity type that uses bundles, the 'bundle_label' annotation gives
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index e92a3d664349e58c01cb299ea43ae979b19711c8..d4ed47c37c1ba58366f6bff66575e7d06da59b96 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -59,20 +59,20 @@ protected function getExpectedNormalizedEntity() {
         'type' => [
           'href' => $this->baseUrl . '/rest/type/node/camelids',
         ],
-        $this->baseUrl . '/rest/relation/node/camelids/uid' => [
+        $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [
           [
             'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
-            'lang' => 'en',
           ],
         ],
-        $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [
+        $this->baseUrl . '/rest/relation/node/camelids/uid' => [
           [
             'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
+            'lang' => 'en',
           ],
         ],
       ],
       '_embedded' => [
-        $this->baseUrl . '/rest/relation/node/camelids/uid' => [
+        $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [
           [
             '_links' => [
               'self' => [
@@ -85,10 +85,9 @@ protected function getExpectedNormalizedEntity() {
             'uuid' => [
               ['value' => $author->uuid()]
             ],
-            'lang' => 'en',
           ],
         ],
-        $this->baseUrl . '/rest/relation/node/camelids/revision_uid' => [
+        $this->baseUrl . '/rest/relation/node/camelids/uid' => [
           [
             '_links' => [
               'self' => [
@@ -101,6 +100,7 @@ protected function getExpectedNormalizedEntity() {
             'uuid' => [
               ['value' => $author->uuid()]
             ],
+            'lang' => 'en',
           ],
         ],
       ],
diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index c11a18889a3953ea6fb25387aab7b0e204edb9c5..b66628c2f34e13a079f688e3a20f0e5ed7add241 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -2,9 +2,7 @@
 
 namespace Drupal\node\Entity;
 
-use Drupal\Core\Entity\ContentEntityBase;
-use Drupal\Core\Entity\EntityChangedTrait;
-use Drupal\Core\Entity\EntityPublishedTrait;
+use Drupal\Core\Entity\EditorialContentEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
@@ -79,10 +77,7 @@
  *   }
  * )
  */
-class Node extends ContentEntityBase implements NodeInterface {
-
-  use EntityChangedTrait;
-  use EntityPublishedTrait;
+class Node extends EditorialContentEntityBase implements NodeInterface {
 
   /**
    * Whether the node is being previewed or not.
@@ -283,21 +278,6 @@ public function setOwner(UserInterface $account) {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionCreationTime() {
-    return $this->get('revision_timestamp')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionCreationTime($timestamp) {
-    $this->set('revision_timestamp', $timestamp);
-    return $this;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -305,13 +285,6 @@ public function getRevisionAuthor() {
     return $this->getRevisionUser();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionUser() {
-    return $this->get('revision_uid')->entity;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -320,50 +293,11 @@ public function setRevisionAuthorId($uid) {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionUser(UserInterface $user) {
-    $this->set('revision_uid', $user);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionUserId() {
-    return $this->get('revision_uid')->entity->id();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionUserId($user_id) {
-    $this->set('revision_uid', $user_id);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRevisionLogMessage() {
-    return $this->get('revision_log')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setRevisionLogMessage($revision_log_message) {
-    $this->set('revision_log', $revision_log_message);
-    return $this;
-  }
-
   /**
    * {@inheritdoc}
    */
   public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     $fields = parent::baseFieldDefinitions($entity_type);
-    $fields += static::publishedBaseFieldDefinitions($entity_type);
 
     $fields['title'] = BaseFieldDefinition::create('string')
       ->setLabel(t('Title'))
@@ -455,30 +389,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ])
       ->setDisplayConfigurable('form', TRUE);
 
-    $fields['revision_timestamp'] = BaseFieldDefinition::create('created')
-      ->setLabel(t('Revision timestamp'))
-      ->setDescription(t('The time that the current revision was created.'))
-      ->setRevisionable(TRUE);
-
-    $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference')
-      ->setLabel(t('Revision user ID'))
-      ->setDescription(t('The user ID of the author of the current revision.'))
-      ->setSetting('target_type', 'user')
-      ->setRevisionable(TRUE);
-
-    $fields['revision_log'] = BaseFieldDefinition::create('string_long')
-      ->setLabel(t('Revision log message'))
-      ->setDescription(t('Briefly describe the changes you have made.'))
-      ->setRevisionable(TRUE)
-      ->setDefaultValue('')
-      ->setDisplayOptions('form', [
-        'type' => 'string_textarea',
-        'weight' => 25,
-        'settings' => [
-          'rows' => 4,
-        ],
-      ]);
-
     $fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
       ->setLabel(t('Revision translation affected'))
       ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 6cd5b6b6eedd60355ed083d8a0c596e0b779472c..80d8a061004443281d243d1eb806b45ca154a7e4 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -900,19 +900,15 @@ public function testPatch() {
 
 
     // DX: 403 when sending PATCH request with read-only fields.
-    // First send all fields (the "maximum normalization"). Assert the expected
-    // error message for the first PATCH-protected field. Remove that field from
-    // the normalization, send another request, assert the next PATCH-protected
-    // field error message. And so on.
-    $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
-    for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) {
-      $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i));
-      $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
+    foreach (static::$patchProtectedFieldNames as $field_name) {
+      $normalization = $this->getNormalizedPatchEntity() + [$field_name => [['value' => $this->randomString()]]];
+      $request_options[RequestOptions::BODY] = $this->serializer->serialize($normalization, static::$format);
       $response = $this->request('PATCH', $url, $request_options);
-      $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response);
+      $this->assertResourceErrorResponse(403, "Access denied on updating field '$field_name'.", $response);
     }
 
     // 200 for well-formed request that sends the maximum number of fields.
+    $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
     $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames);
     $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
     $response = $this->request('PATCH', $url, $request_options);