diff --git a/core/modules/help/help.api.php b/core/modules/help/help.api.php
index 09b2e0f704fba58d45ed1ce546ba59f2cc6ea812..aff491c7ea114f6a190198daf739c1e8c691f192 100644
--- a/core/modules/help/help.api.php
+++ b/core/modules/help/help.api.php
@@ -21,9 +21,9 @@
  * The page-specific help information provided by this hook appears in the
  * Help block (provided by the core Help module), if the block is displayed on
  * that page. The module overview help information is displayed by the Help
- * module. It can be accessed from the page at admin/help or from the Extend
- * Extend page. If a module implements hook_help() the help system expects
- * module overview help to be provided.
+ * module. It can be accessed from the page at /admin/help or from the Extend
+ * page. If a module implements hook_help() the help system expects module
+ * overview help to be provided.
  *
  * For detailed usage examples of:
  * - Module overview help, see content_translation_help(). Module overview
@@ -56,6 +56,25 @@ function hook_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_
   }
 }
 
+/**
+ * Perform alterations on help page section plugin definitions.
+ *
+ * Sections for the page at /admin/help are provided by plugins. This hook
+ * allows modules to alter the plugin definitions.
+ *
+ * @param array $info
+ *   Array of plugin information exposed by hook page section plugins, altered
+ *   by reference.
+ *
+ * @see \Drupal\help\HelpSectionPluginInterface
+ * @see \Drupal\help\Annotation\HelpSection
+ * @see \Drupal\help\HelpSectionManager
+ */
+function hook_help_section_info_alter(&$info) {
+  // Alter the header for the module overviews section.
+  $info['hook_help']['header'] = t('Overviews of modules');
+}
+
 /**
  * @} End of "addtogroup hooks".
  */
diff --git a/core/modules/help/help.module b/core/modules/help/help.module
index 9a7dc07d191b8d0306f0797c542fe03b59210372..c41cd98414a8895ccbf6c1832e6312933a5760aa 100644
--- a/core/modules/help/help.module
+++ b/core/modules/help/help.module
@@ -5,8 +5,8 @@
  * Manages displaying online help.
  */
 
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
@@ -25,7 +25,7 @@ function help_help($route_name, RouteMatchInterface $route_match) {
         $output .= '<li>' . t('<strong>Start posting content</strong> Finally, you may <a href=":content">add new content</a> to your website.', array(':content' => \Drupal::url('node.add_page'))) . '</li>';
       }
       $output .= '</ol>';
-      $output .= '<p>' . t('For more information, refer to the subjects listed in the Help Topics section or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')) . '</p>';
+      $output .= '<p>' . t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')) . '</p>';
       return ['#markup' => $output];
 
     case 'help.page.help':
@@ -43,6 +43,22 @@ function help_help($route_name, RouteMatchInterface $route_match) {
   }
 }
 
+/**
+ * Implements hook_theme().
+ */
+function help_theme($existing, $type, $theme, $path) {
+  return [
+    'help_section' => [
+      'variables' => [
+        'title' => NULL,
+        'description' => NULL,
+        'links' => NULL,
+        'empty' => NULL,
+      ],
+    ],
+  ];
+}
+
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
diff --git a/core/modules/help/help.services.yml b/core/modules/help/help.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..26a2c3376d8e321fd593577049e449aad7143d4d
--- /dev/null
+++ b/core/modules/help/help.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.help_section:
+    class: Drupal\help\HelpSectionManager
+    parent: default_plugin_manager
diff --git a/core/modules/help/src/Annotation/HelpSection.php b/core/modules/help/src/Annotation/HelpSection.php
new file mode 100644
index 0000000000000000000000000000000000000000..d43156d1c7f5bc2af4e2dbfe29790e280ab0824c
--- /dev/null
+++ b/core/modules/help/src/Annotation/HelpSection.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Annotation\HelpSection.
+ */
+
+namespace Drupal\help\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Plugin annotation object for help page section plugins.
+ *
+ * Plugin Namespace: Plugin\HelpSection
+ *
+ * For a working example, see \Drupal\help\Plugin\HelpSection\HookHelpSection.
+ *
+ * @see \Drupal\help\HelpSectionPluginInterface
+ * @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
+ * @see \Drupal\help\HelpSectionManager
+ * @see hook_help_section_info_alter()
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class HelpSection extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The text to use as the title of the help page section.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $title;
+
+  /**
+   * The description of the help page section.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description;
+
+  /**
+   * The (optional) permission needed to view the help section.
+   *
+   * Only set if this section needs its own permission, beyond the generic
+   * 'access administration pages' permission needed to see the /admin/help
+   * page itself.
+   *
+   * @var string
+   */
+  public $permission = '';
+
+}
diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php
index 4758aa0e7bb77f227b22013bd0c00ba5d0790965..61793d59e4bea75381eb51544eae00e5cb0c2249 100644
--- a/core/modules/help/src/Controller/HelpController.php
+++ b/core/modules/help/src/Controller/HelpController.php
@@ -7,9 +7,10 @@
 
 namespace Drupal\help\Controller;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
+use Drupal\help\HelpSectionManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
@@ -25,14 +26,24 @@ class HelpController extends ControllerBase {
    */
   protected $routeMatch;
 
+  /**
+   * The help section plugin manager.
+   *
+   * @var \Drupal\help\HelpSectionManager
+   */
+  protected $helpManager;
+
   /**
    * Creates a new HelpController.
    *
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The current route match.
+   * @param \Drupal\help\HelpSectionManager $help_manager
+   *   The help section manager.
    */
-  public function __construct(RouteMatchInterface $route_match) {
+  public function __construct(RouteMatchInterface $route_match, HelpSectionManager $help_manager) {
     $this->routeMatch = $route_match;
+    $this->helpManager = $help_manager;
   }
 
   /**
@@ -40,62 +51,57 @@ public function __construct(RouteMatchInterface $route_match) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('current_route_match')
+      $container->get('current_route_match'),
+      $container->get('plugin.manager.help_section')
     );
   }
 
   /**
-   * Prints a page listing a glossary of Drupal terminology.
+   * Prints a page listing various types of help.
+   *
+   * The page has sections defined by \Drupal\help\HelpSectionPluginInterface
+   * plugins.
    *
-   * @return string
-   *   An HTML string representing the contents of help page.
+   * @return array
+   *   A render array for the help page.
    */
   public function helpMain() {
-    $output = array(
-      '#markup' => '<h2>' . $this->t('Help topics') . '</h2><p>' . $this->t('Help is available on the following items:') . '</p>',
-      'links' => $this->helpLinksAsList(),
-    );
-    return $output;
-  }
+    $output = [];
 
-  /**
-   * Provides a formatted list of available help topics.
-   *
-   * @return string
-   *   A string containing the formatted list.
-   */
-  protected function helpLinksAsList() {
-    $modules = array();
-    foreach ($this->moduleHandler()->getImplementations('help') as $module) {
-      $modules[$module] = $this->moduleHandler->getName($module);
-    }
-    asort($modules);
-
-    // Output pretty four-column list.
-    $count = count($modules);
-    $break = ceil($count / 4);
-    $column = array(
-      '#type' => 'container',
-      'links' => array('#theme' => 'item_list'),
-      '#attributes' => array('class' => array('layout-column', 'layout-column--quarter')),
-    );
-    $output = array(
-      '#prefix' => '<div class="clearfix">',
-      '#suffix' => '</div>',
-      0 => $column,
-    );
+    // We are checking permissions, so add the user.permissions cache context.
+    $cacheability = new CacheableMetadata();
+    $cacheability->addCacheContexts(['user.permissions']);
 
-    $i = 0;
-    $current_column = 0;
-    foreach ($modules as $module => $name) {
-      $output[$current_column]['links']['#items'][] = $this->l($name, new Url('help.page', array('name' => $module)));
-      if (($i + 1) % $break == 0 && ($i + 1) != $count) {
-        $current_column++;
-        $output[$current_column] = $column;
+    $plugins = $this->helpManager->getDefinitions();
+    $cacheability->addCacheableDependency($this->helpManager);
+
+    foreach ($plugins as $plugin_id => $plugin_definition) {
+      // Check the provided permission.
+      if (!empty($plugin_definition['permission']) && !$this->currentuser()->hasPermission($plugin_definition['permission'])) {
+        continue;
+      }
+
+      // Add the section to the page.
+      /** @var \Drupal\help\HelpSectionPluginInterface $plugin */
+      $plugin = $this->helpManager->createInstance($plugin_id);
+      $this_output = [
+        '#theme' => 'help_section',
+        '#title' => $plugin->getTitle(),
+        '#description' => $plugin->getDescription(),
+        '#empty' => $this->t('There is currently nothing in this section.'),
+        '#links' => [],
+      ];
+
+      $links = $plugin->listTopics();
+      if (is_array($links) && count($links)) {
+        $this_output['#links'] = $links;
       }
-      $i++;
+
+      $cacheability->addCacheableDependency($plugin);
+      $output[$plugin_id] = $this_output;
     }
 
+    $cacheability->applyTo($output);
     return $output;
   }
 
diff --git a/core/modules/help/src/HelpSectionManager.php b/core/modules/help/src/HelpSectionManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..45dfa3caa4283acfc9b05f9369650adbaa5b5821
--- /dev/null
+++ b/core/modules/help/src/HelpSectionManager.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpSectionManager.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages help page section plugins.
+ *
+ * @see \Drupal\help\HelpSectionPluginInterface
+ * @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
+ * @see \Drupal\help\Annotation\HelpSection
+ * @see hook_help_section_info_alter()
+ */
+class HelpSectionManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new HelpSectionManager.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler for the alter hook.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/HelpSection', $namespaces, $module_handler, 'Drupal\help\HelpSectionPluginInterface', 'Drupal\help\Annotation\HelpSection');
+
+    $this->alterInfo('help_section_info');
+    $this->setCacheBackend($cache_backend, 'help_section_plugins');
+  }
+
+}
diff --git a/core/modules/help/src/HelpSectionPluginInterface.php b/core/modules/help/src/HelpSectionPluginInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..49bc463c8195cb79fe55000edef9b0dbe044cfc3
--- /dev/null
+++ b/core/modules/help/src/HelpSectionPluginInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpSectionPluginInterface.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+
+/**
+ * Provides an interface for a plugin for a section of the /admin/help page.
+ *
+ * Plugins of this type need to be annotated with
+ * \Drupal\help\Annotation\HelpSection annotation, and placed in the
+ * Plugin\HelpSection namespace directory. They are managed by the
+ * \Drupal\help\HelpSectionManager plugin manager class. There is a base
+ * class that may be helpful:
+ * \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase.
+ */
+interface HelpSectionPluginInterface extends PluginInspectionInterface, CacheableDependencyInterface {
+
+
+  /**
+   * Returns the title of the help section.
+   *
+   * @return string
+   *   The title text, which could be a plain string or an object that can be
+   *   cast to a string.
+   */
+  public function getTitle();
+
+  /**
+   * Returns the description text for the help section.
+   *
+   * @return string
+   *   The description text, which could be a plain string or an object that
+   *   can be cast to a string.
+   */
+  public function getDescription();
+
+  /**
+   * Returns a list of topics to show in the help section.
+   *
+   * @return array
+   *   A sorted list of topic links or render arrays for topic links. The links
+   *   will be shown in the help section; if the returned array of links is
+   *   empty, the section will be shown with some generic empty text.
+   */
+  public function listTopics();
+
+}
diff --git a/core/modules/help/src/Plugin/HelpSection/HelpSectionPluginBase.php b/core/modules/help/src/Plugin/HelpSection/HelpSectionPluginBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5934737314953c68f3eaface19a10c19b587fe7
--- /dev/null
+++ b/core/modules/help/src/Plugin/HelpSection/HelpSectionPluginBase.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase.
+ */
+
+namespace Drupal\help\Plugin\HelpSection;
+
+use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\help\HelpSectionPluginInterface;
+
+/**
+ * Provides a base class for help section plugins.
+ *
+ * @see \Drupal\help\HelpSectionPluginInterface
+ * @see \Drupal\help\Annotation\HelpSection
+ * @see \Drupal\help\HelpSectionManager
+ */
+abstract class HelpSectionPluginBase extends PluginBase implements HelpSectionPluginInterface {
+
+  use UnchangingCacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->getPluginDefinition()['title'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->getPluginDefinition()['description'];
+  }
+
+}
diff --git a/core/modules/help/src/Plugin/HelpSection/HookHelpSection.php b/core/modules/help/src/Plugin/HelpSection/HookHelpSection.php
new file mode 100644
index 0000000000000000000000000000000000000000..e4cf4eba2b976f1d4d1745a58d9b5e7d0e2017ae
--- /dev/null
+++ b/core/modules/help/src/Plugin/HelpSection/HookHelpSection.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Plugin\HelpSection\HookHelpSection.
+ */
+
+namespace Drupal\help\Plugin\HelpSection;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the module topics list section for the help page.
+ *
+ * @HelpSection(
+ *   id = "hook_help",
+ *   title = @Translation("Module overviews"),
+ *   description = @Translation("Module overviews are provided by modules. Overviews available for your installed modules:"),
+ * )
+ */
+class HookHelpSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a HookHelpSection object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function listTopics() {
+    $topics = [];
+    foreach ($this->moduleHandler->getImplementations('help') as $module) {
+      $title = $this->moduleHandler->getName($module);
+      $topics[$title] = Link::createFromRoute($title, 'help.page', ['name' => $module]);
+    }
+
+    // Sort topics by title, which is the array key above.
+    ksort($topics);
+    return $topics;
+  }
+
+}
diff --git a/core/modules/help/src/Tests/HelpTest.php b/core/modules/help/src/Tests/HelpTest.php
index 64ad4a773fbf4ede46c7d4b83af6fae0d40ee4c9..4f90cb735b16f530031e3f3992748c19c8e6cee8 100644
--- a/core/modules/help/src/Tests/HelpTest.php
+++ b/core/modules/help/src/Tests/HelpTest.php
@@ -20,11 +20,12 @@ class HelpTest extends WebTestBase {
    * Modules to enable.
    *
    * The help_test module implements hook_help() but does not provide a module
-   * overview page.
+   * overview page. The help_page_test module has a page section plugin that
+   * returns no links.
    *
    * @var array.
    */
-  public static $modules = array('help_test');
+  public static $modules = array('help_test', 'help_page_test');
 
   /**
    * Use the Standard profile to test help implementations of many core modules.
@@ -52,7 +53,7 @@ protected function setUp() {
   }
 
   /**
-   * Logs in users, creates dblog events, and tests dblog functionality.
+   * Logs in users, tests help pages.
    */
   public function testHelp() {
     // Login the root user to ensure as many admin links appear as possible on
@@ -67,10 +68,16 @@ public function testHelp() {
     // Verify that introductory help text exists, goes for 100% module coverage.
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/help');
-    $this->assertRaw(t('For more information, refer to the subjects listed in the Help Topics section or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')), 'Help intro text correctly appears.');
+    $this->assertRaw(t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')));
 
-    // Verify that help topics text appears.
-    $this->assertRaw('<h2>' . t('Help topics') . '</h2><p>' . t('Help is available on the following items:') . '</p>', 'Help topics text correctly appears.');
+    // Verify that hook_help() section title and description appear.
+    $this->assertRaw('<h2>' . t('Module overviews') . '</h2>');
+    $this->assertRaw('<p>' . t('Module overviews are provided by modules. Overviews available for your installed modules:'), '</p>');
+
+    // Verify that an empty section is handled correctly.
+    $this->assertRaw('<h2>' . t('Empty section') . '</h2>');
+    $this->assertRaw('<p>' . t('This description should appear.'), '</p>');
+    $this->assertText(t('There is currently nothing in this section.'));
 
     // Make sure links are properly added for modules implementing hook_help().
     foreach ($this->getModuleList() as $module => $name) {
@@ -81,10 +88,25 @@ public function testHelp() {
     // handled correctly.
     $this->clickLink(\Drupal::moduleHandler()->getName('help_test'));
     $this->assertRaw(t('No help is available for module %module.', array('%module' => \Drupal::moduleHandler()->getName('help_test'))));
+
+    // Verify that the order of topics is alphabetical by displayed module
+    // name, by checking the order of some modules, including some that would
+    // have a different order if it was done by machine name instead.
+    $this->drupalGet('admin/help');
+    $page_text = $this->getTextContent();
+    $start = strpos($page_text, 'Module overviews');
+    $pos = $start;
+    $list = ['Block', 'Color', 'Custom Block', 'History', 'Text Editor'];
+    foreach ($list as $name) {
+      $this->assertLink($name);
+      $new_pos = strpos($page_text, $name, $start);
+      $this->assertTrue($new_pos > $pos, 'Order of ' . $name . ' is correct on page');
+      $pos = $new_pos;
+    }
   }
 
   /**
-   * Verifies the logged in user has access to the various help nodes.
+   * Verifies the logged in user has access to the various help pages.
    *
    * @param int $response
    *   (optional) An HTTP response code. Defaults to 200.
@@ -100,7 +122,7 @@ protected function verifyHelp($response = 200) {
     }
 
     foreach ($this->getModuleList() as $module => $name) {
-      // View module help node.
+      // View module help page.
       $this->drupalGet('admin/help/' . $module);
       $this->assertResponse($response);
       if ($response == 200) {
diff --git a/core/modules/help/src/Tests/NoHelpTest.php b/core/modules/help/src/Tests/NoHelpTest.php
index f0213a96284f6370de50c1864f5545035fb3e935..cce40b2db111f8355aea70730ea6a73e03d958d9 100644
--- a/core/modules/help/src/Tests/NoHelpTest.php
+++ b/core/modules/help/src/Tests/NoHelpTest.php
@@ -43,7 +43,7 @@ public function testMainPageNoHelp() {
 
     $this->drupalGet('admin/help');
     $this->assertResponse(200);
-    $this->assertText('Help is available on the following items', 'Help page is found.');
+    $this->assertText('Module overviews are provided by modules');
     $this->assertFalse(\Drupal::moduleHandler()->implementsHook('menu_test', 'help'), 'The menu_test module does not implement hook_help');
     $this->assertNoText(\Drupal::moduleHandler()->getName('menu_test'), 'Making sure the test module menu_test does not display a help link on admin/help.');
 
diff --git a/core/modules/help/templates/help-section.html.twig b/core/modules/help/templates/help-section.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..9d63b4b06fb0e570fd65aa9cde5da6085f666f6a
--- /dev/null
+++ b/core/modules/help/templates/help-section.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a section of the help page.
+ *
+ * Available variables:
+ * - title: The section title.
+ * - description: The description text for the section.
+ * - links: Links to display in the section.
+ * - empty: Text to display if there are no links.
+ *
+ * @ingroup themeable
+ */
+#}
+<h2>{{ title }}</h2>
+<p>{{ description }}</p>
+{% if links %}
+  <ul>
+  {% for link in links %}
+    <li>{{ link }}</li>
+  {% endfor %}
+  </ul>
+{% else %}
+  <p>{{ empty }}</p>
+{% endif %}
diff --git a/core/modules/help/tests/modules/help_page_test/src/Plugin/HelpSection/EmptyHelpSection.php b/core/modules/help/tests/modules/help_page_test/src/Plugin/HelpSection/EmptyHelpSection.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7adac7ab297b22fe8ce6a23e3d3791a29848d04
--- /dev/null
+++ b/core/modules/help/tests/modules/help_page_test/src/Plugin/HelpSection/EmptyHelpSection.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help_page_test\Plugin\HelpSection\EmptyHelpSection.
+ */
+
+namespace Drupal\help_page_test\Plugin\HelpSection;
+
+use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
+
+/**
+ * Provides an empty section for the help page, for testing.
+ *
+ * @HelpSection(
+ *   id = "empty_section",
+ *   title = @Translation("Empty section"),
+ *   description = @Translation("This description should appear."),
+ * )
+ */
+class EmptyHelpSection extends HelpSectionPluginBase {
+  /**
+   * {@inheritdoc}
+   */
+  public function listTopics() {
+    return [];
+  }
+
+}
diff --git a/core/modules/tour/src/Plugin/HelpSection/TourHelpSection.php b/core/modules/tour/src/Plugin/HelpSection/TourHelpSection.php
new file mode 100644
index 0000000000000000000000000000000000000000..49c5da219a093d4f50db0f3ece4c30c1dd67d95c
--- /dev/null
+++ b/core/modules/tour/src/Plugin/HelpSection/TourHelpSection.php
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\tour\Plugin\HelpSection\TourHelpSection.
+ */
+
+namespace Drupal\tour\Plugin\HelpSection;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the tours list section for the help page.
+ *
+ * @HelpSection(
+ *   id = "tour",
+ *   title = @Translation("Tours"),
+ *   description = @Translation("Tours guide you through workflows or explain concepts on various user interface pages. The tours with links in this list are on user interface landing pages; the tours without links will show on individual pages (such as when editing a View using the Views UI module). Available tours:"),
+ *   permission = "access tour"
+ * )
+ */
+class TourHelpSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a TourHelpSection object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    // The calculation of which URL (if any) gets put on which tour depends
+    // on a route access check. This can have a lot of inputs, including user
+    // permissions and other factors. Rather than doing a complicated
+    // accounting of the cache metadata for all of these possible factors, set
+    // the max age of the cache to zero to prevent using incorrect cached
+    // information.
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function listTopics() {
+    /** @var \Drupal\tour\TourInterface[] $tours */
+    $tours = $this->entityTypeManager->getStorage('tour')->loadMultiple();
+    // Sort in the manner defined by Tour.
+    uasort($tours, ['Drupal\tour\Entity\Tour', 'sort']);
+
+    // Make a link to each tour, using the first of its routes that can
+    // be linked to by this user, if any.
+    $topics = [];
+    foreach ($tours as $tour) {
+      $title = $tour->label();
+      $id = $tour->id();
+      $routes = $tour->getRoutes();
+      $made_link = FALSE;
+      foreach ($routes as $route) {
+        // Some tours are for routes with parameters. For instance, there is
+        // currently a tour in the Language module for the language edit page,
+        // which appears on all pages with URLs like:
+        // /admin/config/regional/language/edit/LANGCODE.
+        // There is no way to make a link to the page that displays the tour,
+        // because it is a set of pages. The easiest way to detect this is to
+        // use a try/catch exception -- try to make a link, and it will error
+        // out with a missing parameter exception if the route leads to a set
+        // of pages instead of a single page.
+        try {
+          $params = isset($route['route_params']) ? $route['route_params'] : [];
+          $url = Url::fromRoute($route['route_name'], $params);
+          // Skip this route if the current user cannot access it.
+          if (!$url->access()) {
+            continue;
+          }
+
+          // Generate the link HTML directly, using toString(), to catch
+          // missing parameter exceptions now instead of at render time.
+          $topics[$id] = Link::fromTextAndUrl($title, $url)->toString();
+          // If the line above didn't generate an exception, we have a good
+          // link that the user can access.
+          $made_link = TRUE;
+          break;
+        }
+        catch (\Exception $e) {
+          // Exceptions are normally due to routes that need parameters. If
+          // there is an exception, just try the next route and see if we can
+          // find one that will work for us.
+        }
+      }
+      if (!$made_link) {
+        // None of the routes worked to make a link, so at least display the
+        // tour title.
+        $topics[$id] = $title;
+      }
+    }
+
+    return $topics;
+  }
+
+}
diff --git a/core/modules/tour/src/Tests/TourHelpPageTest.php b/core/modules/tour/src/Tests/TourHelpPageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a39d6a67591f02815ced59cd90f95abbd6cb691d
--- /dev/null
+++ b/core/modules/tour/src/Tests/TourHelpPageTest.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\tour\Tests\TourHelpPageTest.
+ */
+
+namespace Drupal\tour\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Verifies help page display of tours.
+ *
+ * @group help
+ */
+class TourHelpPageTest extends WebTestBase {
+
+  /**
+   * Modules to enable, including some providing tours.
+   *
+   * @var array
+   */
+  public static $modules = ['help', 'tour', 'locale', 'language'];
+
+  /**
+   * User that can access tours and help.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $tourUser;
+
+  /**
+   * A user who can access help but not tours.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $noTourUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create users. For the Tour user, include permissions for the language
+    // tours' parent pages, but not the translation tour's parent page. See
+    // self:getTourList().
+    $this->tourUser = $this->drupalCreateUser(['access administration pages', 'access tour', 'administer languages']);
+    $this->noTourUser = $this->drupalCreateUser(['access administration pages']);
+  }
+
+  /**
+   * Logs in users, tests help pages.
+   */
+  public function testHelp() {
+    $this->drupalLogin($this->tourUser);
+    $this->verifyHelp();
+
+    $this->drupalLogin($this->noTourUser);
+    $this->verifyHelp(FALSE);
+  }
+
+  /**
+   * Verifies the logged in user has access to the help properly.
+   *
+   * @param bool $tours_ok
+   *   (optional) TRUE (default) if the user should see tours, FALSE if not.
+   */
+  protected function verifyHelp($tours_ok = TRUE) {
+    $this->drupalGet('admin/help');
+
+    // All users should be able to see the module section.
+    $this->assertText('Module overviews are provided by modules');
+    foreach ($this->getModuleList() as $name) {
+      $this->assertLink($name);
+    }
+
+    // Some users should be able to see the tour section.
+    if ($tours_ok) {
+      $this->assertText('Tours guide you through workflows');
+    }
+    else {
+      $this->assertNoText('Tours guide you through workflows');
+    }
+
+    $titles = $this->getTourList();
+
+    // Test the titles that should be links.
+    foreach ($titles[0] as $title) {
+      if ($tours_ok) {
+        $this->assertLink($title);
+      }
+      else {
+        $this->assertNoLink($title);
+        // Just test the first item in the list of links that should not
+        // be there, because the second matches the name of a module that is
+        // in the Module overviews section, so the link will be there and
+        // this test will fail. Testing one should be sufficient to verify
+        // the page is working correctly.
+        break;
+      }
+    }
+
+    // Test the titles that should not be links.
+    foreach ($titles[1] as $title) {
+      if ($tours_ok) {
+        $this->assertText($title);
+        $this->assertNoLink($title);
+      }
+      else {
+        $this->assertNoText($title);
+        // Just test the first item in the list of text that should not
+        // be there, because the second matches part of the name of a module
+        // that is in the Module overviews section, so the text will be there
+        // and this test will fail. Testing one should be sufficient to verify
+        // the page is working correctly.
+        break;
+      }
+    }
+  }
+
+  /**
+   * Gets a list of modules to test for hook_help() pages.
+   *
+   * @return array
+   *   A list of module names to test.
+   */
+  protected function getModuleList() {
+    return ['Help', 'Tour'];
+  }
+
+  /**
+   * Gets a list of tours to test.
+   *
+   * @return array
+   *   A list of tour titles to test. The first array element is a list of tours
+   *   with links, and the second is a list of tours without links. Assumes
+   *   that the user being tested has 'administer languages' permission but
+   *   not 'translate interface'.
+   */
+  protected function getTourList() {
+    return [['Adding languages', 'Language'], ['Editing languages', 'Translation']];
+  }
+
+}
diff --git a/core/themes/classy/templates/misc/help-section.html.twig b/core/themes/classy/templates/misc/help-section.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..6cfaa38da26655da2c5e1915b603563fc877accc
--- /dev/null
+++ b/core/themes/classy/templates/misc/help-section.html.twig
@@ -0,0 +1,48 @@
+{#
+/**
+ * @file
+ * Theme override for a section of the help page.
+ *
+ * This implementation divides the links into 4 columns.
+ *
+ * Available variables:
+ * - title: The section title.
+ * - description: The description text for the section.
+ * - links: Links to display in the section.
+ * - empty: Text to display if there are no links.
+ */
+#}
+<div class="clearfix">
+  <h2>{{ title }}</h2>
+  <p>{{ description }}</p>
+  {% if links %}
+    {# Calculate the column length, to divide links into 4 columns. #}
+    {% set size = links|length // 4 %}
+    {% if size * 4 < links|length %}
+      {% set size = size + 1 %}
+    {% endif %}
+
+    {# Output the links in 4 columns. #}
+    {% set count = 0 %}
+    {% for link in links %}
+      {% if count == 0 %}
+        {# Start a new column. #}
+        <div class="layout-column layout-column--quarter"><ul>
+      {% endif %}
+      <li>{{ link }}</li>
+      {% set count = count + 1 %}
+      {% if count >= size %}
+        {# End the current column. #}
+        {% set count = 0 %}
+        </ul></div>
+      {% endif %}
+    {% endfor %}
+
+    {# End the last column, if one is open. #}
+    {% if count > 0 %}
+      </ul></div>
+    {% endif %}
+  {% else %}
+    <p>{{ empty }}</p>
+  {% endif %}
+</div>
diff --git a/core/themes/stable/templates/admin/help-section.html.twig b/core/themes/stable/templates/admin/help-section.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..4b0d7bd84b07421fc2669754df6d0e5a37862b26
--- /dev/null
+++ b/core/themes/stable/templates/admin/help-section.html.twig
@@ -0,0 +1,23 @@
+{#
+/**
+ * @file
+ * Theme override for a section of the help page.
+ *
+ * Available variables:
+ * - title: The section title.
+ * - description: The description text for the section.
+ * - links: Links to display in the section.
+ * - empty: Text to display if there are no links.
+ */
+#}
+<h2>{{ title }}</h2>
+<p>{{ description }}</p>
+{% if links %}
+  <ul>
+  {% for link in links %}
+    <li>{{ link }}</li>
+  {% endfor %}
+  </ul>
+{% else %}
+  <p>{{ empty }}</p>
+{% endif %}