diff --git a/core/modules/help_topics/help_topics/core.security.html.twig b/core/modules/help_topics/help_topics/core.security.html.twig
index 63b12c4..6c04f7d 100644
--- a/core/modules/help_topics/help_topics/core.security.html.twig
+++ b/core/modules/help_topics/help_topics/core.security.html.twig
@@ -1,4 +1,4 @@
 <meta name="help_topic:label" content="Making your site secure"/>
 <meta name="help_topic:top_level"/>
-<meta name="help_topic:related" content="menu_ui.menu_overview"/>
+<meta name="help_topic:related" content="core.menu_overview"/>
 <p>{% trans %}The topics listed here will help you make and keep your site secure.{% endtrans %}</p>
diff --git a/core/modules/help_topics/tests/src/Functional/HelpTopicsSyntaxTest.php b/core/modules/help_topics/tests/src/Functional/HelpTopicsSyntaxTest.php
new file mode 100644
index 0000000..7b1f76c
--- /dev/null
+++ b/core/modules/help_topics/tests/src/Functional/HelpTopicsSyntaxTest.php
@@ -0,0 +1,230 @@
+<?php
+
+namespace Drupal\Tests\help_topics\Functional;
+
+use Drupal\Component\Discovery\DiscoveryException;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\help_topics\HelpTopicDiscovery;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * Verifies that all core Help topics can be rendered and comply with standards.
+ *
+ * @todo This test should eventually be folded into
+ * Drupal\Tests\system\Functional\Module\InstallUninstallTest
+ * when help_topics becomes stable, so that it will test with only one module
+ * at a time installed and not duplicate the effort of installing. See issue
+ * https://www.drupal.org/project/drupal/issues/3074040
+ *
+ * @group help_topics
+ */
+class HelpTopicsSyntaxTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'help',
+    'help_topics',
+  ];
+
+  /**
+   * Tests that all Core help topics can be rendered and have good syntax.
+   */
+  public function testHelpTopics() {
+    $this->drupalLogin($this->rootUser);
+
+    // Enable all modules and themes, so that all routes mentioned in topics
+    // will be defined.
+    $module_directories = $this->listDirectories('module');
+    $this->container->get('module_installer')->install(array_keys($module_directories));
+    $theme_directories = $this->listDirectories('theme');
+    $this->container->get('theme_installer')->install(array_keys($theme_directories));
+
+    $directories = $module_directories + $theme_directories +
+      $this->listDirectories('profile');
+    $directories['core'] = 'core/help_topics';
+
+    // Verify that a few key modules, themes, and profiles are listed, so that
+    // we can be certain our directory list is complete and we will be testing
+    // all existing help topics. If these lines in the test fail in the future,
+    // it is probably because something we chose to list here is being removed.
+    // Substitute another item of the same type that still exists, so that this
+    // test can continue.
+    $this->assertTrue(isset($directories['system']), 'System module is being scanned');
+    $this->assertTrue(isset($directories['help']), 'Help module is being scanned');
+    $this->assertTrue(isset($directories['seven']), 'Seven theme is being scanned');
+    $this->assertTrue(isset($directories['standard']), 'Standard profile is being scanned');
+
+    $definitions = $this->discoverAllTopics($directories);
+    $this->assertTrue(count($definitions) > 1, 'At least 1 topic was found');
+    $top_level = $this->findTopLevel($definitions);
+
+    // Test each topic for compliance with standards.
+    foreach (array_keys($definitions) as $id) {
+      $this->verifyTopic($id, $definitions, $top_level);
+    }
+
+    // Create a single topic that is expected to fail discovery.
+    vfsStream::setup('root');
+    vfsStream::create([
+      'modules' => [
+        'test' => [
+          'help_topics' => [
+            // An invalid topic missing required details.
+            'test.topic.html.twig' => '',
+          ],
+        ],
+      ],
+    ]);
+    $fail_directories['expected_failure'] = vfsStream::url('root/modules/test/help_topics');
+
+    // Check that just this expected failure topic does throw an exception.
+    $this->expectException(DiscoveryException::class);
+    $this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig should begin with 'expected_failure.'");
+    $this->discoverAllTopics($fail_directories);
+  }
+
+  /**
+   * Verifies rendering and standards compliance of one help topic.
+   *
+   * @param string $id
+   *   ID of the topic to verify.
+   * @param array $definitions
+   *   Array of all topic definitions, keyed by ID.
+   * @param array $top_level
+   *   Array of top-level topic definitions, keyed by ID.
+   */
+  protected function verifyTopic($id, $definitions, $top_level) {
+    $definition = $definitions[$id];
+
+    // Visit the URL for the topic.
+    $this->drupalGet('admin/help/topic/' . $id);
+
+    // Verify the title and response.
+    $session = $this->assertSession();
+    $session->statusCodeEquals(200);
+    $session->titleEquals($definition['label'] . ' | Drupal');
+
+    // Verify that all the related topics exist. Also check to see if any of
+    // them are top-level (we will need that in the next section).
+    $has_top_level_related = FALSE;
+    if (isset($definition['related'])) {
+      foreach ($definition['related'] as $related_id) {
+        $this->assertTrue(isset($definitions[$related_id]), 'Topic ' . $id . ' is only related to topics that exist (' . $related_id . ')');
+        $has_top_level_related = $has_top_level_related || isset($top_level[$related_id]);
+      }
+    }
+
+    // Verify this is either top-level or related to a top-level topic.
+    $this->assertTrue(isset($top_level[$id]) || $has_top_level_related,
+      'Topic ' . $id . ' is either top-level or related to at least one other top-level topic');
+
+    // Verify that the label is not empty.
+    $this->assertTrue(isset($definition['label']) && !empty($definition['label']), 'Topic ' . $id . ' has a non-empty label');
+
+    // Read in the file so we can run some tests on that.
+    $body = file_get_contents($definition[HelpTopicDiscovery::FILE_KEY]);
+    $this->assertNotEmpty($body, 'Topic ' . $id . ' has a non-empty Twig file');
+
+    // Remove the meta tags (already tested above), and Twig set and variable
+    // printouts from the file.
+    $body = preg_replace('|\<meta.*\/\>|sU', '', $body);
+    $body = preg_replace('|\{\{.*\}\}|sU', '', $body);
+    $body = preg_replace('|\{\% set.*\%\}|sU', '', $body);
+    $this->assertNotEmpty($body, 'Topic ' . $id . ' Twig file contains some text outside of meta tags');
+
+    // Verify that if we remove all the translated text, whitespace, and
+    // HTML tags, there is nothing left (that is, all text is translated).
+    $text = preg_replace('|\{\% trans \%\}.*\{\% endtrans \%\}|sU', '', $body);
+    $text = strip_tags($text);
+    $text = preg_replace('|\s+|', '', $text);
+    $this->assertEmpty($text, 'Topic ' . $id . ' Twig file has all of its text translated');
+
+    // Load the topic body as HTML and verify that it parses.
+    $doc = new \DOMDocument();
+    $doc->strictErrorChecking = TRUE;
+    $doc->validateOnParse = TRUE;
+    $doc->loadHTML($body);
+
+    // Check for headings hierarchy.
+    $levels = [1, 2, 3, 4, 5, 6];
+    foreach ($levels as $level) {
+      $num_headings[$level] = $doc->getElementsByTagName('h' . $level)->length;
+      if ($level == 1) {
+        $this->assertTrue($num_headings[1] == 0, 'Topic ' . $id . ' has no H1 tag');
+        // Set num_headings to 1 for this level, so the rest of the hierarchy
+        // can be tested using simpler code.
+        $num_headings[1] = 1;
+      }
+      else {
+        // We should either not have this heading, or if we do have one at this
+        // level, we should also have the next-smaller level. That is, if we
+        // have an h3, we should have also had an h2.
+        $this->assertTrue($num_headings[$level - 1] > 0 || $num_headings[$level] == 0,
+          'Topic ' . $id . ' has the correct H2-H6 heading hierarchy');
+      }
+    }
+  }
+
+  /**
+   * Lists the extension directories of a certain type.
+   *
+   * @param string $type
+   *   The type of extension to list: module, theme, or profile.
+   *
+   * @return string[]
+   *   An array of all of the directories of this type of extension, keyed by
+   *   extension short name.
+   */
+  protected function listDirectories($type) {
+    $directories = [];
+
+    // Find the extensions of this type, even if they are not installed, but
+    // excluding test ones.
+    $lister = \Drupal::service('extension.list.' . $type);
+    foreach (array_keys($lister->getAllAvailableInfo()) as $name) {
+      $path = $lister->getPath($name);
+      // You can tell test modules because they are in package 'Testing', but
+      // test themes are only known by being found in test directories. So...
+      // exclude things in test directories.
+      if ((strpos($path, '/tests') === FALSE) &&
+        (strpos($path, '/testing') === FALSE)) {
+        $directories[$name] = $path . '/help_topics';
+      }
+    }
+    return $directories;
+  }
+
+  /**
+   * Discovers the help topics in a list of locations.
+   *
+   * @param string[] $directories
+   *   An array of directories to find topics in, keyed by extension short name.
+   *
+   * @return array
+   *   Array of all topic definitions, keyed by topic ID, from non-test
+   *   modules, themes, and profiles.
+   */
+  protected function discoverAllTopics(array $directories) {
+    $discovery = new HelpTopicDiscovery($directories);
+    return $discovery->getDefinitions();
+  }
+
+  /**
+   * Finds the list of top-level topics.
+   *
+   * @param array $definitions
+   *   An array of topic definitions, keyed by ID.
+   *
+   * @return array
+   *   The items from $definitions that represent top-level topics, still
+   *   keyed by ID.
+   */
+  protected function findTopLevel(array $definitions) {
+    return array_filter($definitions, function ($definition) {
+      return isset($definition['top_level']) && $definition['top_level'];
+    });
+  }
+
+}
