diff --git a/core/modules/help/config/install/filter.format.help.yml b/core/modules/help/config/install/filter.format.help.yml
new file mode 100644
index 0000000..7f87b2e
--- /dev/null
+++ b/core/modules/help/config/install/filter.format.help.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+name: Help
+format: help
+weight: 0
+filters:
+  filter_html:
+    id: filter_html
+    provider: filter
+    status: true
+    weight: -10
+    settings:
+      allowed_html: '<a> <img> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h3> <h4> <h5> <h6>'
+      filter_html_help: true
+      filter_html_nofollow: false
diff --git a/core/modules/help/config/install/help.topic.help.yml b/core/modules/help/config/install/help.topic.help.yml
new file mode 100644
index 0000000..b898631
--- /dev/null
+++ b/core/modules/help/config/install/help.topic.help.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies: {  }
+id: help
+label: 'Help module'
+body:
+  value: "<h3>About</h3>\r\n<p>The Help module provides <a href=\"[route:help.main]\">Help reference pages</a> to guide you through the use and configuration of modules. It is a starting point for <a href=\"https://www.drupal.org/documentation\">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href=\"https://www.drupal.org/documentation/modules/help/\">online documentation for the Help module</a>.</p>\r\n<h3>Uses</h3>\r\n<dl>\r\n<dt>Providing a help reference</dt>\r\n<dd>The Help module displays both static module-provided help and configured help topics on the main <a href=\"[route:help.main]\">Help page</a>.</dd>\r\n<dt>Configuring help topics</dt>\r\n<dd>You can add, edit, delete, and translate configure help topics on the <a href=\"[route:help.topic_admin]\">Help topics</a> administration page. The help topics that are listed in the Module help section of the main Help page cannot be edited or deleted.</dd>\r\n</dl>\r\n"
+  format: help
+top_level: true
+related: {  }
+list_on: {  }
diff --git a/core/modules/help/config/schema/help.schema.yml b/core/modules/help/config/schema/help.schema.yml
new file mode 100644
index 0000000..4922897
--- /dev/null
+++ b/core/modules/help/config/schema/help.schema.yml
@@ -0,0 +1,33 @@
+help.topic.*:
+  type: config_entity
+  label: 'Help topic'
+  mapping:
+    id:
+      type: string
+      label: 'Machine-readable name'
+    label:
+      type: label
+      label: 'Title'
+    body:
+      label: 'Body'
+      type: mapping
+      mapping:
+        value:
+          type: text
+          label: 'Body'
+        format:
+          type: string
+          label: 'Text format'
+    top_level:
+      type: boolean
+      label: 'Top-level topic'
+    related:
+      type: sequence
+      label: 'Related topics'
+      sequence:
+        - type: string
+    list_on:
+      type: sequence
+      label: 'List on topics'
+      sequence:
+        - type: string
diff --git a/core/modules/help/help.links.action.yml b/core/modules/help/help.links.action.yml
new file mode 100644
index 0000000..3b452f0
--- /dev/null
+++ b/core/modules/help/help.links.action.yml
@@ -0,0 +1,5 @@
+entity.help_topic.add_form:
+  route_name: entity.help_topic.add_form
+  title: 'Add new help topic'
+  appears_on:
+    - help.topic_admin
diff --git a/core/modules/help/help.links.menu.yml b/core/modules/help/help.links.menu.yml
index aa85add..aa9c469 100644
--- a/core/modules/help/help.links.menu.yml
+++ b/core/modules/help/help.links.menu.yml
@@ -4,3 +4,9 @@ help.main:
   route_name: help.main
   weight: 9
   parent: system.admin
+
+help.topic_admin:
+  title: Help topics
+  description: Add, delete, and edit help topics.
+  route_name: help.topic_admin
+  parent: system.admin_config_development
diff --git a/core/modules/help/help.links.task.yml b/core/modules/help/help.links.task.yml
new file mode 100644
index 0000000..e2ce418
--- /dev/null
+++ b/core/modules/help/help.links.task.yml
@@ -0,0 +1,9 @@
+entity.help_topic.canonical:
+  title: 'View'
+  route_name: entity.help_topic.canonical
+  base_route: entity.help_topic.canonical
+
+entity.help_topic.edit_form:
+  title: 'Edit'
+  route_name: entity.help_topic.edit_form
+  base_route: entity.help_topic.canonical
diff --git a/core/modules/help/help.module b/core/modules/help/help.module
index 3539660..81dfdad 100644
--- a/core/modules/help/help.module
+++ b/core/modules/help/help.module
@@ -26,17 +26,6 @@ function help_help($route_name, RouteMatchInterface $route_match) {
       $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://drupal.org/documentation', '!support' => 'https://drupal.org/support', '!drupal' => 'https://drupal.org')) . '</p>';
       return $output;
-
-    case 'help.page.help':
-      $output = '';
-      $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t('The Help module provides <a href="!help-page">Help reference pages</a> to guide you through the use and configuration of modules. It is a starting point for <a href="!handbook">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href="!help">online documentation for the Help module</a>.', array('!help' => 'https://drupal.org/documentation/modules/help/', '!handbook' => 'https://drupal.org/documentation', '!help-page' => \Drupal::url('help.main'))) . '</p>';
-      $output .= '<h3>' . t('Uses') . '</h3>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Providing a help reference') . '</dt>';
-      $output .= '<dd>' . t('The Help module displays explanations for using each module listed on the main <a href="!help">Help reference page</a>.', array('!help' => \Drupal::url('help.main'))) . '</dd>';
-      $output .= '</dl>';
-      return $output;
   }
 }
 
diff --git a/core/modules/help/help.permissions.yml b/core/modules/help/help.permissions.yml
new file mode 100644
index 0000000..c1bc85a
--- /dev/null
+++ b/core/modules/help/help.permissions.yml
@@ -0,0 +1,3 @@
+administer help topics:
+  title: 'Administer help topics'
+  description: 'Create, edit, and delete help topics.'
diff --git a/core/modules/help/help.routing.yml b/core/modules/help/help.routing.yml
index a393eb8..3540943 100644
--- a/core/modules/help/help.routing.yml
+++ b/core/modules/help/help.routing.yml
@@ -13,3 +13,43 @@ help.page:
     _title: 'Help'
   requirements:
     _permission: 'access administration pages'
+
+entity.help_topic.canonical:
+  path: '/admin/help-topic/{help_topic}'
+  defaults:
+    _entity_view: 'help_topic.full'
+    _title: 'Help'
+  requirements:
+    _entity_access: 'help_topic.view'
+
+help.topic_admin:
+  path: '/admin/config/development/help'
+  defaults:
+    _entity_list: 'help_topic'
+    _title: 'Help topics'
+  requirements:
+    _permission: 'administer help topics'
+
+entity.help_topic.add_form:
+   path: '/admin/config/development/help/add'
+   defaults:
+     _entity_form: 'help_topic.add'
+     _title: 'Add help topic'
+   requirements:
+     _entity_create_access: 'help_topic'
+
+entity.help_topic.edit_form:
+   path: '/admin/config/development/help/{help_topic}/edit'
+   defaults:
+     _entity_form: 'help_topic.edit'
+     _title: 'Edit help topic'
+   requirements:
+     _entity_access: 'help_topic.edit'
+
+entity.help_topic.delete_form:
+   path: '/admin/config/development/help/{help_topic}/delete'
+   defaults:
+     _entity_form: 'help_topic.delete'
+     _title: 'Delete help topic'
+   requirements:
+     _entity_access: 'help_topic.delete'
diff --git a/core/modules/help/help.services.yml b/core/modules/help/help.services.yml
new file mode 100644
index 0000000..5a43fbf
--- /dev/null
+++ b/core/modules/help/help.services.yml
@@ -0,0 +1,6 @@
+services:
+  help.breadcrumb:
+    class: Drupal\help\HelpBreadcrumbBuilder
+    arguments: ['@string_translation']
+    tags:
+      - { name: breadcrumb_builder, priority: 900 }
diff --git a/core/modules/help/help.tokens.inc b/core/modules/help/help.tokens.inc
new file mode 100644
index 0000000..9854c62
--- /dev/null
+++ b/core/modules/help/help.tokens.inc
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for help topics.
+ *
+ * This file handles tokens for the global 'help_topic' tokens.
+ */
+
+use Drupal\help\Entity\HelpTopic;
+
+/**
+ * Implements hook_token_info().
+ */
+function help_token_info() {
+  $types['help_topic'] = array(
+    'name' => t("Help Topics"),
+    'description' => t("Provides tokens for help topics."),
+  );
+
+  $topics = array();
+  /* @var \Drupal\help\HelpTopicInterface $help_topic */
+  foreach (HelpTopic::loadMultiple() as $help_topic_id => $help_topic) {
+    $topics[$help_topic_id] = array(
+      'name' => $help_topic->label(),
+      'description' => t('URL to the @label help topic', array('@label' => $help_topic->label())),
+    );
+  }
+
+  return array(
+    'types' => $types,
+    'tokens' => array(
+      'help_topic' => $topics,
+    ),
+  );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function help_tokens($type, $tokens, array $data = array(), array $options = array()) {
+  $replacements = array();
+
+  if ($type == 'help_topic') {
+    foreach ($tokens as $name => $original) {
+      if ($topic = HelpTopic::load($name)) {
+        $replacements[$original] = $topic->url('canonical');
+      }
+    }
+  }
+
+  return $replacements;
+}
diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php
index 2de0a53..0453375 100644
--- a/core/modules/help/src/Controller/HelpController.php
+++ b/core/modules/help/src/Controller/HelpController.php
@@ -8,11 +8,12 @@
 namespace Drupal\help\Controller;
 
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
+use Drupal\Component\Utility\String;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Drupal\Component\Utility\String;
 
 /**
  * Controller routines for help routes.
@@ -27,13 +28,23 @@ class HelpController extends ControllerBase {
   protected $routeMatch;
 
   /**
+   * The help topic entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $helpStorage;
+
+  /**
    * Creates a new HelpController.
    *
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The current route match.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $help_storage
+   *   The entity storage for help topic entities.
    */
-  public function __construct(RouteMatchInterface $route_match) {
+  public function __construct(RouteMatchInterface $route_match, EntityStorageInterface $help_storage) {
     $this->routeMatch = $route_match;
+    $this->helpStorage = $help_storage;
   }
 
   /**
@@ -41,50 +52,91 @@ 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('entity.manager')->getStorage('help_topic')
     );
   }
 
   /**
-   * Prints a page listing a glossary of Drupal terminology.
+   * Prints a page listing help topics.
    *
-   * @return string
-   *   An HTML string representing the contents of help page.
+   * @return array
+   *   Render array for the help topics list.
    */
   public function helpMain() {
     $output = array(
       '#attached' => array(
         'css' => array(drupal_get_path('module', 'help') . '/css/help.module.css'),
       ),
-      '#markup' => '<h2>' . $this->t('Help topics') . '</h2><p>' . $this->t('Help is available on the following items:') . '</p>' . $this->helpLinksAsList(),
     );
+
+    $template = '<h2>{{ title }}</h2><p>{{ header }}</p>{{ links }}';
+
+    $output['modules'] = array(
+      '#type' => 'inline_template',
+      '#template' => $template,
+      '#context' => array(
+        'title' => $this->t('Module help'),
+        'header' => $this->t('Help pages are available for the following modules:'),
+        'links' => array('#markup' => $this->moduleHelpLinksAsList()),
+      ),
+    );
+
+    $output['topics'] = array(
+      '#type' => 'inline_template',
+      '#template' => $template,
+      '#context' => array(
+        'title' => $this->t('Configured topics'),
+        'header' => $this->t('Additional help topics configured on your site:'),
+        'links' => array('#markup' => $this->helpTopicsList()),
+      ),
+    );
+
     return $output;
   }
 
   /**
-   * Provides a formatted list of available help topics.
+   * Provides a formatted list of available module help topics from hook_help().
    *
    * @return string
    *   A string containing the formatted list.
    */
-  protected function helpLinksAsList() {
+  protected function moduleHelpLinksAsList() {
     $module_info = system_rebuild_module_data();
 
     $modules = array();
     foreach ($this->moduleHandler()->getImplementations('help') as $module) {
       if ($this->moduleHandler()->invoke($module, 'help', array("help.page.$module", $this->routeMatch))) {
-        $modules[$module] = $module_info[$module]->info['name'];
+        $modules[$module] = array(
+          'title' => $module_info[$module]->info['name'],
+          'url' => new Url('help.page', array('name' => $module)),
+        );
       }
     }
     asort($modules);
 
+    return $this->fourColumnList($modules);
+  }
+
+  /**
+   * Makes a four-column list of links.
+   *
+   * @param array $links
+   *   Array whose elements are arrays with:
+   *   - title: Link title.
+   *   - url: URL object.
+   *
+   * @return string
+   *   Markup for a four-column UL list of links.
+   */
+  protected function fourColumnList($links) {
     // Output pretty four-column list.
-    $count = count($modules);
+    $count = count($links);
     $break = ceil($count / 4);
     $output = '<div class="clearfix"><div class="help-items"><ul>';
     $i = 0;
-    foreach ($modules as $module => $name) {
-      $output .= '<li>' . $this->l($name, new Url('help.page', array('name' => $module))) . '</li>';
+    foreach ($links as $info) {
+      $output .= '<li>' . $this->l($info['title'], $info['url']) . '</li>';
       if (($i + 1) % $break == 0 && ($i + 1) != $count) {
         $output .= '</ul></div><div class="help-items' . ($i + 1 == $break * 3 ? ' help-items-last' : '') . '"><ul>';
       }
@@ -96,13 +148,37 @@ protected function helpLinksAsList() {
   }
 
   /**
-   * Prints a page listing general help for a module.
+   * Provides a formatted list of configured help topics.
+   *
+   * @return string
+   *   A string containing the formatted list.
+   */
+  protected function helpTopicsList() {
+    $entities = $this->helpStorage->loadMultiple();
+    uasort($entities, array('Drupal\help\Entity\HelpTopic', 'sort'));
+
+    $topics = array();
+    /* @var \Drupal\help\HelpTopicInterface $entity */
+    foreach ($entities as $entity) {
+      if ($entity->isTopLevel()) {
+        $topics[] = array(
+          'title' => $entity->label(),
+          'url' => $entity->urlInfo('canonical'),
+        );
+      }
+    }
+
+    return $this->fourColumnList($topics);
+  }
+
+  /**
+   * Renders a help page from a module's hook_help().
    *
    * @param string $name
    *   A module name to display a help page for.
    *
    * @return array
-   *   A render array as expected by drupal_render().
+   *   A render array for the help page.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
    */
@@ -144,5 +220,4 @@ public function helpPage($name) {
       throw new NotFoundHttpException();
     }
   }
-
 }
diff --git a/core/modules/help/src/Entity/HelpTopic.php b/core/modules/help/src/Entity/HelpTopic.php
new file mode 100644
index 0000000..7c7131d
--- /dev/null
+++ b/core/modules/help/src/Entity/HelpTopic.php
@@ -0,0 +1,180 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Entity\HelpTopic.
+ */
+
+namespace Drupal\help\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\help\HelpTopicInterface;
+
+/**
+ * Defines a configuration entity for help topics.
+ *
+ * Developers can provide configurable help topics for their modules, themes,
+ * etc. by creating help topics from admin/config/development/help (with the
+ * Help module installed), and then exporting them into their config/install
+ * directories. Topics marked as "top_level" will be listed on admin/help,
+ * and when viewing a help topic, "related" topics will be listed. Conventions:
+ * - Module overview help topics should have titles like "Foo Bar module";
+ *   similar for overview topics for themes, install profiles, etc.
+ * - Module overview topics should follow
+ *   @link https://drupal.org/node/632280 the standard help template. @endlink
+ * - Non-overview help topics should usually not be top-level topics; instead,
+ *   make an overview topic and list other topics in the "related" section.
+ * - All topic machine names should be prefixed by the extension machine name.
+ *   The main topic for an extension can just be the bare machine name.
+ * - Keep in mind that help topics are usually meant for administrative Drupal
+ *   users, such as site builders and content editors, rather than programmers.
+ *
+ * @ConfigEntityType(
+ *   id = "help_topic",
+ *   label = @Translation("Help topic"),
+ *   config_prefix = "topic",
+ *   handlers = {
+ *     "form" = {
+ *       "add" = "Drupal\help\Form\HelpTopicForm",
+ *       "edit" = "Drupal\help\Form\HelpTopicForm",
+ *       "delete" = "Drupal\help\Form\HelpDeleteForm",
+ *     },
+ *     "list_builder" = "Drupal\help\HelpListBuilder",
+ *     "view_builder" = "Drupal\help\HelpViewBuilder",
+ *     "access" = "Drupal\help\HelpAccessControlHandler",
+ *   },
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   admin_permission = "administer help topics",
+ *   links = {
+ *     "canonical" = "entity.help_topic.canonical",
+ *     "add-form" = "entity.help_topic.add_form",
+ *     "edit-form" = "entity.help_topic.edit_form",
+ *     "delete-form" = "entity.help_topic.delete_form"
+ *   }
+ * )
+ */
+class HelpTopic extends ConfigEntityBase implements HelpTopicInterface {
+  /**
+   * The topic machine name.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The topic title.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The unfiltered topic body text.
+   *
+   * This is an array with two elements:
+   * - value: The unfiltered text.
+   * - format: The ID of the filter format.
+   *
+   * @var array
+   */
+  protected $body;
+
+  /**
+   * Whether or not the topic should appear on the help topics list.
+   *
+   * @var bool
+   */
+  protected $top_level;
+
+  /**
+   * List of related topic machine names.
+   *
+   * @var string[]
+   */
+  protected $related = array();
+
+  /**
+   * List of topics this one should be listed on.
+   *
+   * @var string[]
+   */
+  protected $list_on = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBody() {
+    return $this->get('body');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTopLevel() {
+    return $this->get('top_level');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRelated() {
+    return $this->get('related');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRelated($topics) {
+    $topics = $this->cleanTopicList($topics);
+    $this->set('related', $topics);
+
+    return $this;
+  }
+
+  /**
+   * Cleans a list of topic IDs.
+   *
+   * @param string|array $topics
+   *   Either an array of topic IDs or a comma-separated list in a string.
+   *
+   * @return array
+   *   List of non-empty IDs from the input.
+   */
+  protected function cleanTopicList($topics) {
+    if (is_string($topics)) {
+      $topics = explode(',', $topics);
+    }
+
+    // Make a list of non-empty trimmed IDs.
+    $tosave = array();
+    foreach ($topics as $item) {
+      $item = trim($item);
+      if ($item) {
+        $tosave[] = $item;
+      }
+    }
+
+    return $tosave;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getListOn() {
+    return $this->get('list_on');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setListOn($topics) {
+    $topics = $this->cleanTopicList($topics);
+    $this->set('list_on', $topics);
+
+    return $this;
+  }
+}
+
diff --git a/core/modules/help/src/Form/HelpDeleteForm.php b/core/modules/help/src/Form/HelpDeleteForm.php
new file mode 100644
index 0000000..f98163f
--- /dev/null
+++ b/core/modules/help/src/Form/HelpDeleteForm.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Form\HelpDeleteForm.
+ */
+
+namespace Drupal\help\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a form for confirming delete of a help topic.
+ */
+class HelpDeleteForm extends EntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the topic %name?', array('%name' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('help.topic_admin');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->entity->delete();
+    drupal_set_message(t('Deleted help topic %name.', array('%name' => $this->entity->label())));
+
+    $form_state->setRedirectUrl($this->getCancelUrl());
+  }
+}
diff --git a/core/modules/help/src/Form/HelpTopicForm.php b/core/modules/help/src/Form/HelpTopicForm.php
new file mode 100644
index 0000000..3b2c9b3
--- /dev/null
+++ b/core/modules/help/src/Form/HelpTopicForm.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Form\HelpTopicForm.
+ */
+
+namespace Drupal\help\Form;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for editing a help topic.
+ */
+class HelpTopicForm extends EntityForm {
+
+  /**
+   * The help topic entity storage.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
+   */
+  protected $helpStorage;
+
+  /**
+   * Constructs a new help topic form.
+   *
+   * @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $help_storage
+   *   The help topic entity storage
+   */
+  public function __construct(ConfigEntityStorageInterface $help_storage) {
+    $this->helpStorage = $help_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('help_topic')
+    );
+  }
+
+  /**
+   * Checks for an existing help topic.
+   *
+   * @param string $entity_id
+   *   The entity ID.
+   *
+   * @return bool
+   *   TRUE if this topic already exists, FALSE otherwise.
+   */
+  public function exists($entity_id) {
+    return (bool) $this->helpStorage
+      ->getQuery()
+      ->condition('id', $entity_id)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Title'),
+      '#maxlength' => 100,
+      '#default_value' => $this->entity->label(),
+      '#required' => TRUE,
+    );
+
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $this->entity->id(),
+      '#machine_name' => array(
+        'exists' => array($this, 'exists'),
+        'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'),
+      ),
+    );
+
+    $form['top_level'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $this->entity->isTopLevel(),
+      '#title' => $this->t('Top-level topic'),
+      '#description' => $this->t('Check box if this topic should be displayed on the topics list'),
+    );
+
+    $body = $this->entity->getBody();
+    if (!isset($body['format'])) {
+      $body = array('value' => '', 'format' => 'help');
+    }
+
+    $form['body'] = array(
+      '#type' => 'text_format',
+      '#title' => $this->t('Body'),
+      '#default_value' => $body['value'],
+      '#format' => $body['format'],
+    );
+
+    $form['related'] = array(
+      '#title' => $this->t('Related topics'),
+      '#description' => $this->t('Comma-separated list of machine names of related topics.'),
+      '#type' => 'textarea',
+      '#default_value' => implode(',', $this->entity->getRelated()),
+    );
+
+    $form['list_on'] = array(
+      '#title' => $this->t('Topics to list this topic on'),
+      '#description' => $this->t('Comma-separated list of machine names of topics. This topic will be listed as Related on those topic pages.'),
+      '#type' => 'textarea',
+      '#default_value' => implode(',', $this->entity->getListOn()),
+    );
+
+    $form['#entity_builders'][] = array($this, 'copyTopicFieldsToEntity');
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * Copies the topics field values to the entity properties.
+   *
+   * This is added to $form['#entity_builders'] in the form builder method.
+   *
+   * @param string $type
+   *   Type of entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   Entity to copy property values to.
+   * @param array $form
+   *   Form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Form state.
+   */
+  protected function copyTopicFieldsToEntity($type, EntityInterface $entity, array &$form, FormStateInterface &$form_state) {
+    $entity->setRelated($form_state->getValue('related'));
+    $entity->setListOn($form_state->getValue('list_on'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $form_state->setRedirect('help.topic_admin');
+
+    $status = $this->entity->save();
+    if ($status == SAVED_UPDATED) {
+      drupal_set_message($this->t('Help topic updated.'));
+    }
+    else {
+      drupal_set_message($this->t('Help topic added.'));
+    }
+  }
+}
diff --git a/core/modules/help/src/HelpAccessControlHandler.php b/core/modules/help/src/HelpAccessControlHandler.php
new file mode 100644
index 0000000..43043dc
--- /dev/null
+++ b/core/modules/help/src/HelpAccessControlHandler.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpAccessControlHandler.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Determines entity access for Help topic entities.
+ */
+class HelpAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    // The default for the parent class is fine for CRUD operations. But
+    // for viewing, use the same permission as for viewing help topics provided
+    // by hook_help().
+    if ($operation == 'view') {
+      return AccessResult::allowedIfHasPermission($account, 'access administration pages');
+    }
+
+    return parent::checkAccess($entity, $operation, $langcode, $account);
+  }
+}
diff --git a/core/modules/help/src/HelpBreadcrumbBuilder.php b/core/modules/help/src/HelpBreadcrumbBuilder.php
new file mode 100644
index 0000000..a1a0d57
--- /dev/null
+++ b/core/modules/help/src/HelpBreadcrumbBuilder.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpBreadcrumbBuilder.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Provides a breadcrumb builder for help topic pages.
+ */
+class HelpBreadcrumbBuilder implements BreadcrumbBuilderInterface {
+  use StringTranslationTrait;
+
+  /**
+   * Constructs the BookBreadcrumbBuilder.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The translation service.
+   */
+  public function __construct(TranslationInterface $string_translation) {
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(RouteMatchInterface $route_match) {
+    return $route_match->getRouteName() == 'entity.help_topic.canonical';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(RouteMatchInterface $route_match) {
+    $links = array(
+      Link::createFromRoute($this->t('Home'), '<front>'),
+      Link::createFromRoute($this->t('Administration'), 'system.admin'),
+      Link::createFromRoute($this->t('Help'), 'help.main'),
+    );
+
+    return $links;
+  }
+
+}
diff --git a/core/modules/help/src/HelpListBuilder.php b/core/modules/help/src/HelpListBuilder.php
new file mode 100644
index 0000000..833756a
--- /dev/null
+++ b/core/modules/help/src/HelpListBuilder.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpListBuilder.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides an entity list builder class for Help topic entities.
+ */
+class HelpListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = array();
+
+    $header['label'] = $this->t('Title');
+    $header['id'] = $this->t('Machine name');
+
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row = array();
+
+    $row['label']['data'] = array(
+      '#type' => 'link',
+      '#title' => $this->getLabel($entity),
+      '#url' => $entity->urlInfo('canonical'),
+    );
+
+    $row['id'] = $entity->id();
+
+    return $row + parent::buildRow($entity);
+  }
+}
diff --git a/core/modules/help/src/HelpTopicInterface.php b/core/modules/help/src/HelpTopicInterface.php
new file mode 100644
index 0000000..978aec6
--- /dev/null
+++ b/core/modules/help/src/HelpTopicInterface.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpTopicInterface.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Defines an interface for a help topic entity.
+ */
+interface HelpTopicInterface extends ConfigEntityInterface {
+
+  /**
+   * Returns the body of the topic.
+   *
+   * @return array
+   *   Array with elements:
+   *   - value: Unformatted, unfiltered text.
+   *   - format: ID of the text format to use to filter/format the text.
+   */
+  public function getBody();
+
+  /**
+   * Returns whether this is a top-level topic or not.
+   *
+   * @return bool
+   *   TRUE if this is a topic that should be displayed on the Help topics
+   *   list; FALSE if not.
+   */
+  public function isTopLevel();
+
+  /**
+   * Returns the IDs of related topics.
+   *
+   * @return array
+   *   Array of the IDs of related topics.
+   */
+  public function getRelated();
+
+  /**
+   * Sets the related topics.
+   *
+   * @param string|array
+   *   Either an array of related topic IDs, or a comma-separated list of
+   *   related topic IDs.
+   *
+   * @return $this
+   */
+  public function setRelated($topics);
+
+  /**
+   * Returns the IDs of topics this should be listed on.
+   *
+   * @return array
+   *   Array of the IDs of topics that should list this one as "related".
+   */
+  public function getListOn();
+
+  /**
+   * Sets the list on topics.
+   *
+   * @param string|array
+   *   Either an array of topic IDs, or a comma-separated list of topic IDs;
+   *   this topic should be listed as Related on those topic pages.
+   *
+   * @return $this
+   */
+  public function setListOn($topics);
+
+}
diff --git a/core/modules/help/src/HelpViewBuilder.php b/core/modules/help/src/HelpViewBuilder.php
new file mode 100644
index 0000000..f27d9a7
--- /dev/null
+++ b/core/modules/help/src/HelpViewBuilder.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\HelpViewBuilder.
+ */
+
+namespace Drupal\help;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Utility\Token;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a help topic view builder.
+ */
+class HelpViewBuilder extends EntityViewBuilder {
+
+  /**
+   * The token replacement service class.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * The entity query factory class.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $queryFactory;
+
+  /**
+   * Creates a new HelpViewBuilder.
+   *
+   * @param \Drupal\Entity\EntityTypeInterface $entity_type
+   *   The entity type object.
+   * @param \Drupal\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager object.
+   * @param \Drupal\Language\LanguageManagerInterface $language_manager
+   *   The language manager object.
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token replacement service class.
+   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
+   *   The query factory service class.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, Token $token, QueryFactory $query_factory) {
+    $this->entityTypeId = $entity_type->id();
+    $this->entityType = $entity_type;
+    $this->entityManager = $entity_manager;
+    $this->languageManager = $language_manager;
+    $this->token = $token;
+    $this->queryFactory = $query_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static($entity_type, $container->get('entity.manager'), $container->get('language_manager'), $container->get('token'), $container->get('entity.query'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
+    $output = array();
+
+    /** @var \Drupal\help\HelpTopicInterface[] $entities */
+    foreach ($entities as $entity_id => $help_topic) {
+      $build = array(
+        '#langcode' => $langcode,
+        '#title' => String::checkPlain($help_topic->label()),
+      );
+
+      $body = $help_topic->getBody();
+      $build['body'] = array(
+        '#type' => 'processed_text',
+        '#text' => $this->token->replace($body['value']),
+        '#format' => $body['format'],
+      );
+
+      // The list should include topics this entity lists as related, plus
+      // topics that have said "Add me to this topic's related list".
+      $related = $help_topic->getRelated() +
+        $this->queryFactory->get('help_topic')
+          ->condition('list_on.*', $help_topic->id())
+          ->execute();
+
+      $links = array();
+      $storage = $this->entityManager->getStorage('help_topic');
+
+      foreach ($related as $other_id) {
+        if ($other_id != $help_topic->id()) {
+          $topic = $storage->load($other_id);
+          if ($topic) {
+            $links[$other_id] = array(
+              'title' => $topic->label(),
+              'url' => $topic->urlInfo('canonical'),
+            );
+          }
+        }
+      }
+
+      if (count($links)) {
+        ksort($links);
+        $build['related'] = array(
+          '#theme' => 'links',
+          '#heading' => array(
+            'text' => $this->t('Related topics'),
+            'level' => 'h3',
+          ),
+          '#links' => $links,
+        );
+      }
+
+      $output[$entity_id] = $build;
+    }
+
+    return $output;
+  }
+}
diff --git a/core/modules/help/src/Tests/HelpTest.php b/core/modules/help/src/Tests/HelpTest.php
index 7010f74..90ae0a4 100644
--- a/core/modules/help/src/Tests/HelpTest.php
+++ b/core/modules/help/src/Tests/HelpTest.php
@@ -16,15 +16,16 @@
  */
 class HelpTest extends WebTestBase {
 
+  // Install with the standard profile, because it has the help block
+  // enabled and admin theme, etc.
+  protected $profile = 'standard';
+
   /**
    * Modules to enable.
    *
    * @var array.
    */
-  public static $modules = array('shortcut');
-
-  // Tests help implementations of many arbitrary core modules.
-  protected $profile = 'standard';
+  public static $modules = array('help_test');
 
   /**
    * The admin user that will be created.
@@ -39,20 +40,19 @@ class HelpTest extends WebTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->getModuleList();
-
     // Create users.
-    $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer permissions'));
+    $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer permissions', 'administer help topics'));
     $this->anyUser = $this->drupalCreateUser(array());
   }
 
   /**
-   * Logs in users, creates dblog events, and tests dblog functionality.
+   * Logs in users, tests help pages.
    */
   public function testHelp() {
     // Login the admin user.
     $this->drupalLogin($this->adminUser);
     $this->verifyHelp();
+    $this->verifyHelpLinks();
 
     // Login the regular user.
     $this->drupalLogin($this->anyUser);
@@ -67,12 +67,18 @@ public function testHelp() {
     $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://drupal.org/documentation', '!support' => 'https://drupal.org/support', '!drupal' => 'https://drupal.org')), 'Help intro text correctly appears.');
 
     // 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.');
+    $this->assertRaw('<h2>' . t('Module help') . '</h2><p>' . t('Help pages are available for the following modules:') . '</p>', 'Help module topics text correctly appears.');
+    $this->assertRaw('<h2>' . t('Configured topics') . '</h2><p>' . t('Additional help topics configured on your site:') . '</p>', 'Help configured topics text correctly appears.');
 
     // Make sure links are properly added for modules implementing hook_help().
     foreach ($this->getModuleList() as $module => $name) {
       $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help/@module)', array('@module' => $module, '@name' => $name)));
     }
+
+    // Make sure links are properly added for topics.
+    foreach ($this->getTopicList() as $topic => $name) {
+      $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help-topic/@topic)', array('@topic' => $topic, '@name' => $name)));
+    }
   }
 
   /**
@@ -100,20 +106,68 @@ protected function verifyHelp($response = 200) {
         $this->assertRaw('<h1 class="page-title">' . t($name) . '</h1>', format_string('%module heading was displayed', array('%module' => $module)));
       }
     }
+
+    foreach ($this->getTopicList() as $topic => $name) {
+      // View module help node.
+      $this->drupalGet('admin/help-topic/' . $topic);
+      $this->assertResponse($response);
+      if ($response == 200) {
+        $this->assertTitle($name . ' | Drupal', format_string('%topic title was displayed', array('%topic' => $topic)));
+        $this->assertRaw('<h1 class="page-title">' . t($name) . '</h1>', format_string('%topic heading was displayed', array('%topic' => $topic)));
+      }
+    }
   }
 
   /**
-   * Gets the list of enabled modules that implement hook_help().
+   * Verifies links on the test help topic page and other pages.
+   *
+   * Assumes an admin user is logged in.
+   */
+  protected function verifyHelpLinks() {
+    // Verify links on the test top-level page.
+    $page = 'admin/help-topic/help_test';
+    $links = array(
+      'link to the Help module topic' => 'The Help module provides',
+      'link to the help admin page' => 'Add new help topic',
+      'Help module' => 'The Help module provides',
+      'Linked topic' => 'This topic is not supposed to be top-level',
+      'Additional topic' => 'This topic should get listed automatically',
+    );
+    foreach ($links as $link_text => $page_text) {
+      $this->drupalGet($page);
+      $this->clickLink($link_text);
+      $this->assertText($page_text);
+    }
+
+    // Verify that the non-top-level topics do not appear on the Help page.
+    $this->drupalGet('admin/help');
+    $this->assertNoLink('Linked topic');
+    $this->assertNoLink('Additional topic');
+  }
+
+  /**
+   * Gets a list of modules to test for hook_help() pages.
    *
    * @return array
-   *   A list of enabled modules.
+   *   A list of modules to test, machine name => displayed name.
    */
   protected function getModuleList() {
-    $modules = array();
-    $module_data = system_rebuild_module_data();
-    foreach (\Drupal::moduleHandler()->getImplementations('help') as $module) {
-      $modules[$module] = $module_data[$module]->info['name'];
-    }
-    return $modules;
+    return array(
+      'help_test' => 'Help Test',
+    );
   }
+
+  /**
+   * Gets a list of topic IDs to test.
+   *
+   * @return array
+   *   A list of topics to test, machine name => displayed name.
+   */
+  protected function getTopicList() {
+    return array(
+      'help' => t('Help module'),
+      'help_test' => t('Help Test module'),
+    );
+  }
+
 }
diff --git a/core/modules/help/src/Tests/HelpTopicAdminTest.php b/core/modules/help/src/Tests/HelpTopicAdminTest.php
new file mode 100644
index 0000000..291fe47
--- /dev/null
+++ b/core/modules/help/src/Tests/HelpTopicAdminTest.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\help\Tests\HelpTopicAdminTest.
+ */
+
+namespace Drupal\help\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the administration interface for help topics.
+ *
+ * @group help
+ */
+class HelpTopicAdminTest extends WebTestBase {
+  /**
+   * Modules to enable.
+   *
+   * @var array.
+   */
+  public static $modules = array('help', 'filter');
+
+  /**
+   * User who can administer help topics and view them.
+   */
+  protected $adminUser;
+
+  /**
+   * Non-admin user to test access to admin pages is blocked.
+   */
+  protected $nonAdminUser;
+
+  protected function setUp() {
+    parent::setUp();
+
+    // Create users.
+    $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer help topics', 'use text format help'));
+    $this->nonAdminUser = $this->drupalCreateUser(array('access administration pages'));
+  }
+
+  /**
+   * Logs in users, tests help admin pages.
+   */
+  public function testHelpAdmin() {
+    $this->drupalLogin($this->adminUser);
+    $this->verifyHelpAdmin();
+
+    $this->drupalLogin($this->nonAdminUser);
+    $this->verifyHelpAdmin(403);
+  }
+
+  /**
+   * Verifies the logged in user has the correct access to help admin.
+   *
+   * @param integer $response
+   *   An HTTP response code to verify.
+   */
+  protected function verifyHelpAdmin($response = 200) {
+    // Verify admin links.
+    foreach(array('admin/config', 'admin/config/development', 'admin/index') as $page) {
+      $this->drupalGet($page);
+      if ($response == 200) {
+        $this->assertText('Add, delete, and edit help topics');
+        $this->assertLink('Help topics');
+      }
+      else {
+        $this->assertNoText('Add, delete, and edit help topics');
+        $this->assertNoLink('Help topics');
+      }
+    }
+
+    // Verify CRUD and listing page.
+    $this->drupalGet('admin/config/development/help');
+    $this->assertResponse($response);
+    if ($response == 200) {
+      $this->assertLink('Add new help topic');
+      $this->assertText('Help topics');
+      $this->assertText('Title');
+      $this->assertText('Machine name');
+      $this->assertText('Operations');
+    }
+
+    $this->drupalGet('admin/config/development/help/add');
+    $this->assertResponse($response);
+
+    // Everything after this point, just do for the admin user.
+    if ($response != 200) {
+      return;
+    }
+
+    // Create a new help topic from the UI.
+    $body = 'This text is for the foo topic';
+    $title = 'Foo topic';
+    $this->drupalPostForm(NULL, array(
+        'label' => $title,
+        'id' => 'foo',
+        'top_level' => TRUE,
+        'body[value]' => $body,
+      ), t('Save'));
+    $this->assertText('Help topic added');
+
+    // Click to view the topic and verify the edit link works too.
+    $this->clickLink($title);
+    $this->assertText($title);
+    $this->assertText($body);
+
+    $this->clickLink(t('Edit'));
+    $new_title = 'Foo longer topic';
+    $this->drupalPostForm(NULL, array('label' => $new_title), t('Save'));
+    $this->assertText('Help topic updated');
+    $this->assertLink($new_title);
+
+    // Verify the link is on the Help page.
+    $this->drupalGet('admin/help');
+    $this->assertLink($new_title);
+
+    // Test deleting.
+    $this->drupalGet('admin/config/development/help/foo/delete');
+    $this->assertText('This action cannot be undone.');
+    $this->assertText('Are you sure you want to delete the topic');
+    $this->assertText($new_title);
+    $this->drupalPostForm(NULL, array(), t('Delete'));
+    $this->assertText('Deleted help topic');
+    $this->assertText($new_title);
+    $this->assertNoLink($new_title);
+    $this->drupalGet('admin/help');
+    $this->assertNoLink($new_title);
+  }
+
+}
diff --git a/core/modules/help/src/Tests/HelpTopicTokensTest.php b/core/modules/help/src/Tests/HelpTopicTokensTest.php
new file mode 100644
index 0000000..95a2405
--- /dev/null
+++ b/core/modules/help/src/Tests/HelpTopicTokensTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\help\Tests\HelpTopicTokensTest.
+ */
+
+namespace Drupal\help\Tests;
+
+use Drupal\Core\Url;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Generates URLs replacements to test token generation for help topics.
+ *
+ * @group system
+ */
+class HelpTopicTokensTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'help');
+
+  /**
+   * Sets up the test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installSchema('system', array('router', 'sequences'));
+    $this->installConfig(array('help'));
+    \Drupal::service('router.builder')->rebuild();
+
+  }
+
+  /**
+   * Tests that help topic tokens work.
+   */
+  public function testHelpTopicTokens() {
+    $text = 'This should <a href="[help_topic:help]">Link to help topic</a>';
+    $replaced = \Drupal::token()->replace($text);
+    $this->assertTrue(strpos($replaced, '<a href="' . Url::fromRoute('entity.help_topic.canonical', [
+      'help_topic' => 'help',
+    ])->toString() . '"') !== FALSE);
+
+    $text = 'This should <a href="[help_topic:nonexistant]">Not link to help topic</a>';
+    $replaced = \Drupal::token()->replace($text);
+    $this->assertTrue(strpos($replaced, '[help_topic:nonexistant]') !== FALSE);
+  }
+
+}
diff --git a/core/modules/help/src/Tests/NoHelpTest.php b/core/modules/help/src/Tests/NoHelpTest.php
index cd4d460..caaf75b 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('Help pages are available for the following modules', 'Help page is found.');
     $this->assertNoText('Hook menu tests', 'Making sure the test module menu_test does not display a help link on admin/help.');
   }
 }
diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml
new file mode 100644
index 0000000..f7ad029
--- /dev/null
+++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml
@@ -0,0 +1,13 @@
+langcode: en
+status: true
+dependencies: {  }
+id: help_test
+label: 'Help Test module'
+body:
+  value: 'This is a test. It should <a href="[help_topic:help]">link to the Help module topic</a>, and it should <a href="[route:help.topic_admin]">link to the help admin page</a>. Also there should be a related topic link below to the Help module topic page and the linked topic.'
+  format: help
+top_level: true
+related:
+  - help
+  - help_test_linked
+list_on: {  }
diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml
new file mode 100644
index 0000000..d638cb3
--- /dev/null
+++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml
@@ -0,0 +1,12 @@
+langcode: en
+status: true
+dependencies: {  }
+id: help_test_additional
+label: 'Additional topic'
+body:
+  value: 'This topic should get listed automatically on the Help test topic.'
+  format: help
+top_level: false
+related: {  }
+list_on:
+  - help_test
diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml
new file mode 100644
index 0000000..8d3674c
--- /dev/null
+++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies: {  }
+id: help_test_linked
+label: 'Linked topic'
+body:
+  value: 'This topic is not supposed to be top-level.'
+  format: help
+top_level: false
+related: {  }
+list_on: {  }
diff --git a/core/modules/help/test/modules/help_test/help_test.info.yml b/core/modules/help/test/modules/help_test/help_test.info.yml
new file mode 100644
index 0000000..e67f988
--- /dev/null
+++ b/core/modules/help/test/modules/help_test/help_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Help Test'
+type: module
+description: 'Support module for help testing.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/help/test/modules/help_test/help_test.module b/core/modules/help/test/modules/help_test/help_test.module
new file mode 100644
index 0000000..f7a918b
--- /dev/null
+++ b/core/modules/help/test/modules/help_test/help_test.module
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Test module for help.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function help_test_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.help_test':
+      return 'Some kind of non-empty output for testing';
+  }
+}
diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php
index 5534d7e..ca83e51 100644
--- a/core/modules/simpletest/src/KernelTestBase.php
+++ b/core/modules/simpletest/src/KernelTestBase.php
@@ -320,7 +320,7 @@ public function containerBuild(ContainerBuilder $container) {
    */
   protected function installConfig(array $modules) {
     foreach ($modules as $module) {
-      if (!$this->container->get('module_handler')->moduleExists($module)) {
+      if ($module != 'core' && !$this->container->get('module_handler')->moduleExists($module)) {
         throw new \RuntimeException(format_string("'@module' module is not enabled.", array(
           '@module' => $module,
         )));
diff --git a/core/modules/system/src/Tests/Token/RouteTokensTest.php b/core/modules/system/src/Tests/Token/RouteTokensTest.php
new file mode 100644
index 0000000..ec19325
--- /dev/null
+++ b/core/modules/system/src/Tests/Token/RouteTokensTest.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Token\RouteTokensTest.
+ */
+
+namespace Drupal\system\Tests\Token;
+
+use Drupal\Core\Url;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Generates URLs replacements to test token generation for routes.
+ *
+ * @group system
+ */
+class RouteTokensTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'user');
+
+  /**
+   * Sets up the test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installSchema('system', array('router', 'sequences'));
+    $this->installConfig(array('core'));
+    \Drupal::service('router.builder')->rebuild();
+
+  }
+
+  /**
+   * Tests that default route tokens work.
+   */
+  public function testRouteTokens() {
+    $text = 'This should <a href="[route:system.admin]">Link to admin</a>';
+    $replaced = \Drupal::token()->replace($text);
+    $this->assertTrue(strpos($replaced, '<a href="' . Url::fromRoute('system.admin')->toString() . '"') !== FALSE);
+
+    $text = 'This should <a href="[route:system.nonexistant]">Not link to admin</a>';
+    $replaced = \Drupal::token()->replace($text);
+    $this->assertTrue(strpos($replaced, '[route:system.nonexistant]') !== FALSE);
+  }
+
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index cd2e6cf..3ac1136 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -883,10 +883,13 @@ function hook_system_info_alter(array &$info, \Drupal\Core\Extension\Extension $
  * developers should usually be provided via function header comments in the
  * code, or in special API example files.
  *
+ * Module overviews and other topic pages can also be provided as configurable
+ * help topics, using the \Drupal\help\Entity\Help config entity, outside of
+ * this hook. See the Help class documentation header for details.
+ *
  * The page-specific help information provided by this hook appears as a system
- * help block 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 page.
+ * help block on that page. The module overview help and configurable help
+ * topics are displayed by the Help module; topics are listed on admin/help.
  *
  * For detailed usage examples of:
  * - Module overview help, see content_translation_help(). Module overview
diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc
index ff78335..6133653 100644
--- a/core/modules/system/system.tokens.inc
+++ b/core/modules/system/system.tokens.inc
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Url;
 
 /**
  * Implements hook_token_info().
@@ -22,7 +23,10 @@ function system_token_info() {
     'name' => t("Dates"),
     'description' => t("Tokens related to times and dates."),
   );
-
+  $types['route'] = array(
+    'name' => t("Route information"),
+    'description' => t("Tokens for route names."),
+  );
   // Site-wide global tokens.
   $site['name'] = array(
     'name' => t("Name"),
@@ -75,11 +79,25 @@ function system_token_info() {
     'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
   );
 
+  $routes = array();
+  /* @var \Symfony\Component\Routing\Route $route */
+  foreach (\Drupal::service('router.route_provider')->getAllRoutes() as $route_name => $route) {
+    if (strpos($route->getPath(), '}') !== FALSE) {
+      // Route tokens don't support route parameters.
+      continue;
+    }
+    $routes[$route_name] = array(
+      'name' => t('URL to !route_name', ['!route_name' => $route_name]),
+      'description' => t('The URL to the !route_name route.', array('!route_name' => $route_name)),
+    );
+  }
+
   return array(
     'types' => $types,
     'tokens' => array(
       'site' => $site,
       'date' => $date,
+      'route' => $routes,
     ),
   );
 }
@@ -124,7 +142,10 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
           break;
 
         case 'url-brief':
-          $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('<front>', array(), $url_options));
+          $replacements[$original] = preg_replace(array(
+            '!^https?://!',
+            '!/$!'
+          ), '', \Drupal::url('<front>', array(), $url_options));
           break;
 
         case 'login-url':
@@ -173,5 +194,20 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
     }
   }
 
+  elseif ($type == 'route') {
+    foreach ($tokens as $route_name => $original) {
+      try {
+        $url = Url::fromRoute($route_name)->toString();
+      }
+      catch (\Exception $e) {
+        // Invalid route, do nothing.
+        $url = FALSE;
+      }
+      if ($url) {
+        $replacements[$original] = $url;
+      }
+    }
+  }
+
   return $replacements;
 }
