diff --git a/.rubocop_todo/gitlab/documentation_links/link.yml b/.rubocop_todo/gitlab/documentation_links/link.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e41a645ac2b915d217db4625df13b36398180c56
--- /dev/null
+++ b/.rubocop_todo/gitlab/documentation_links/link.yml
@@ -0,0 +1,188 @@
+---
+# Cop supports --autocorrect.
+Gitlab/DocumentationLinks/Link:
+  Details: grace period
+  Exclude:
+    - '**/*.haml' # Excluding HAML files so as to not conflict with the DocumentationLinks HAML linter
+    - 'app/controllers/concerns/enforces_two_factor_authentication.rb'
+    - 'app/controllers/import/github_controller.rb'
+    - 'app/controllers/jira_connect/app_descriptor_controller.rb'
+    - 'app/controllers/jwt_controller.rb'
+    - 'app/controllers/metrics_controller.rb'
+    - 'app/controllers/repositories/git_http_client_controller.rb'
+    - 'app/helpers/ci/jobs_helper.rb'
+    - 'app/helpers/ci/pipeline_editor_helper.rb'
+    - 'app/helpers/clusters_helper.rb'
+    - 'app/helpers/container_registry/container_registry_helper.rb'
+    - 'app/helpers/environments_helper.rb'
+    - 'app/helpers/feature_flags_helper.rb'
+    - 'app/helpers/ide_helper.rb'
+    - 'app/helpers/import_helper.rb'
+    - 'app/helpers/integrations_helper.rb'
+    - 'app/helpers/invite_members_helper.rb'
+    - 'app/helpers/issuables_helper.rb'
+    - 'app/helpers/issues_helper.rb'
+    - 'app/helpers/merge_requests_helper.rb'
+    - 'app/helpers/mirror_helper.rb'
+    - 'app/helpers/notes_helper.rb'
+    - 'app/helpers/operations_helper.rb'
+    - 'app/helpers/packages_helper.rb'
+    - 'app/helpers/preferences_helper.rb'
+    - 'app/helpers/projects/security/configuration_helper.rb'
+    - 'app/helpers/projects_helper.rb'
+    - 'app/helpers/releases_helper.rb'
+    - 'app/helpers/search_helper.rb'
+    - 'app/helpers/visibility_level_helper.rb'
+    - 'app/helpers/wiki_helper.rb'
+    - 'app/mailers/emails/identity_verification.rb'
+    - 'app/models/concerns/ci/has_completion_reason.rb'
+    - 'app/models/integration.rb'
+    - 'app/models/integrations/apple_app_store.rb'
+    - 'app/models/integrations/beyond_identity.rb'
+    - 'app/models/integrations/google_play.rb'
+    - 'app/models/integrations/irker.rb'
+    - 'app/models/integrations/jira.rb'
+    - 'app/models/integrations/jira_cloud_app.rb'
+    - 'app/models/integrations/phorge.rb'
+    - 'app/models/integrations/zentao.rb'
+    - 'app/models/key.rb'
+    - 'app/models/user.rb'
+    - 'app/presenters/clusters/cluster_presenter.rb'
+    - 'app/presenters/commit_status_presenter.rb'
+    - 'app/presenters/dev_ops_report/metric_presenter.rb'
+    - 'app/presenters/group_clusterable_presenter.rb'
+    - 'app/presenters/instance_clusterable_presenter.rb'
+    - 'app/presenters/key_presenter.rb'
+    - 'app/presenters/project_clusterable_presenter.rb'
+    - 'app/presenters/projects/security/configuration_presenter.rb'
+    - 'app/serializers/issue_entity.rb'
+    - 'app/serializers/merge_request_noteable_entity.rb'
+    - 'app/serializers/merge_request_widget_entity.rb'
+    - 'app/services/jira/requests/base.rb'
+    - 'app/services/merge_requests/refresh_service.rb'
+    - 'app/services/projects/update_service.rb'
+    - 'ee/app/components/namespaces/combined_storage_users/base_alert_component.rb'
+    - 'ee/app/components/namespaces/free_user_cap/enforcement_alert_component.rb'
+    - 'ee/app/components/namespaces/storage/limit_alert_component.rb'
+    - 'ee/app/components/namespaces/storage/pre_enforcement_alert_component.rb'
+    - 'ee/app/components/namespaces/storage/repository_limit_alert_component.rb'
+    - 'ee/app/controllers/concerns/gitlab_subscriptions/trials/duo_common.rb'
+    - 'ee/app/helpers/billing_plans_helper.rb'
+    - 'ee/app/helpers/dependencies_helper.rb'
+    - 'ee/app/helpers/ee/container_registry/container_registry_helper.rb'
+    - 'ee/app/helpers/ee/form_helper.rb'
+    - 'ee/app/helpers/ee/groups/settings_helper.rb'
+    - 'ee/app/helpers/ee/groups_helper.rb'
+    - 'ee/app/helpers/ee/import_helper.rb'
+    - 'ee/app/helpers/ee/projects_helper.rb'
+    - 'ee/app/helpers/ee/security_orchestration_helper.rb'
+    - 'ee/app/helpers/epics_helper.rb'
+    - 'ee/app/helpers/groups/add_ons/discover_duo_pro_helper.rb'
+    - 'ee/app/helpers/groups/discovers_helper.rb'
+    - 'ee/app/helpers/member_roles_helper.rb'
+    - 'ee/app/helpers/namespaces/free_user_cap_helper.rb'
+    - 'ee/app/helpers/projects/learn_gitlab_helper.rb'
+    - 'ee/app/helpers/projects/security/api_fuzzing_configuration_helper.rb'
+    - 'ee/app/helpers/projects/security/sast_configuration_helper.rb'
+    - 'ee/app/helpers/security_helper.rb'
+    - 'ee/app/helpers/vulnerabilities_helper.rb'
+    - 'ee/app/models/ee/member.rb'
+    - 'ee/app/models/integrations/git_guardian.rb'
+    - 'ee/app/models/integrations/github.rb'
+    - 'ee/app/presenters/ee/merge_request_presenter.rb'
+    - 'ee/app/serializers/epic_entity.rb'
+    - 'ee/app/services/ee/auth/container_registry_authentication_service.rb'
+    - 'ee/app/services/incident_management/create_incident_sla_exceeded_label_service.rb'
+    - 'ee/app/services/merge_requests/update_blocks_service.rb'
+    - 'ee/app/services/merge_trains/refresh_merge_request_service.rb'
+    - 'ee/app/services/search/rake_task_executor_service.rb'
+    - 'ee/app/services/security/security_orchestration_policies/project_create_service.rb'
+    - 'ee/lib/api/managed_licenses.rb'
+    - 'ee/lib/api/member_roles.rb'
+    - 'ee/lib/ee/gitlab/namespace_storage_size_error_message.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/change_failure_rate.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/group/deployment_frequency.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/lead_time.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/lead_time_for_changes.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/time_to_merge.rb'
+    - 'ee/lib/gitlab/analytics/cycle_analytics/summary/time_to_restore_service.rb'
+    - 'ee/lib/gitlab/checks/secrets_check.rb'
+    - 'ee/lib/gitlab/expiring_subscription_message.rb'
+    - 'ee/lib/gitlab/llm/chain/tools/tool.rb'
+    - 'ee/lib/gitlab/llm/embeddings/utils/base_content_parser.rb'
+    - 'ee/lib/gitlab/root_excess_size_error_message.rb'
+    - 'ee/lib/security/scan_result_policies/policy_violation_comment.rb'
+    - 'ee/lib/system_check/geo/current_node_check.rb'
+    - 'ee/spec/components/namespaces/free_user_cap/enforcement_alert_component_spec.rb'
+    - 'ee/spec/components/namespaces/free_user_cap/non_owner_enforcement_alert_component_spec.rb'
+    - 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb'
+    - 'ee/spec/features/admin/admin_show_new_user_signups_cap_alert_spec.rb'
+    - 'ee/spec/features/groups/group_overview_spec.rb'
+    - 'ee/spec/features/merge_request/user_edits_multiple_reviewers_mr_spec.rb'
+    - 'ee/spec/features/projects/show_project_spec.rb'
+    - 'ee/spec/features/search/elastic/project_search_spec.rb'
+    - 'ee/spec/features/search/zoekt/search_spec.rb'
+    - 'ee/spec/features/security/project/discover_spec.rb'
+    - 'ee/spec/helpers/container_registry/container_registry_helper_spec.rb'
+    - 'ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb'
+    - 'ee/spec/helpers/ee/projects/security/sast_configuration_helper_spec.rb'
+    - 'ee/spec/helpers/epics_helper_spec.rb'
+    - 'ee/spec/helpers/member_roles_helper_spec.rb'
+    - 'ee/spec/helpers/projects/learn_gitlab_helper_spec.rb'
+    - 'ee/spec/helpers/projects_helper_spec.rb'
+    - 'ee/spec/helpers/security_helper_spec.rb'
+    - 'ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb'
+    - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/change_failure_rate_spec.rb'
+    - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_for_changes_spec.rb'
+    - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_spec.rb'
+    - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/time_to_restore_service_spec.rb'
+    - 'ee/spec/lib/gitlab/llm/embeddings/utils/base_content_parser_spec.rb'
+    - 'ee/spec/mailers/emails/enterprise_users_spec.rb'
+    - 'ee/spec/requests/api/internal/base_spec.rb'
+    - 'ee/spec/requests/api/member_roles_spec.rb'
+    - 'ee/spec/requests/callout_spec.rb'
+    - 'ee/spec/services/ee/auth/container_registry_authentication_service_spec.rb'
+    - 'ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb'
+    - 'ee/spec/views/admin/application_settings/_ee_package_registry.html.haml_spec.rb'
+    - 'ee/spec/views/groups/add_ons/discover_duo_pro/show.html.haml_spec.rb'
+    - 'ee/spec/views/groups/settings/analytics/_analytics_dashboards.html.haml_spec.rb'
+    - 'ee/spec/views/projects/settings/analytics/_custom_dashboard_projects.html.haml_spec.rb'
+    - 'ee/spec/views/user_settings/profiles/show.html.haml_spec.rb'
+    - 'lib/backup/tasks/database.rb'
+    - 'lib/gitlab/checks/global_file_size_check.rb'
+    - 'lib/gitlab/cycle_analytics/summary/deployment_frequency.rb'
+    - 'lib/gitlab/graphql/deprecations.rb'
+    - 'lib/gitlab/middleware/go.rb'
+    - 'lib/gitlab/security/features.rb'
+    - 'lib/gitlab/usage_data_counters/hll_redis_counter.rb'
+    - 'lib/slack/block_kit/app_home_opened.rb'
+    - 'lib/system_check/helpers.rb'
+    - 'lib/users/internal.rb'
+    - 'lib/web_ide/extensions_marketplace.rb'
+    - 'spec/controllers/import/github_controller_spec.rb'
+    - 'spec/features/admin/admin_sees_background_migrations_spec.rb'
+    - 'spec/features/dashboard/snippets_spec.rb'
+    - 'spec/features/groups/container_registry_spec.rb'
+    - 'spec/features/help_dropdown_spec.rb'
+    - 'spec/features/issues/service_desk_spec.rb'
+    - 'spec/features/projects/container_registry_spec.rb'
+    - 'spec/features/projects/settings/branch_rules_callout_spec.rb'
+    - 'spec/features/projects/settings/registry_settings_spec.rb'
+    - 'spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb'
+    - 'spec/helpers/ci/pipeline_editor_helper_spec.rb'
+    - 'spec/helpers/clusters_helper_spec.rb'
+    - 'spec/helpers/container_registry/container_registry_helper_spec.rb'
+    - 'spec/helpers/ide_helper_spec.rb'
+    - 'spec/helpers/invite_members_helper_spec.rb'
+    - 'spec/helpers/issues_helper_spec.rb'
+    - 'spec/helpers/operations_helper_spec.rb'
+    - 'spec/helpers/projects/security/configuration_helper_spec.rb'
+    - 'spec/lib/gitlab/security/scan_configuration_spec.rb'
+    - 'spec/mailers/emails/pages_domains_spec.rb'
+    - 'spec/mailers/emails/profile_spec.rb'
+    - 'spec/presenters/clusters/cluster_presenter_spec.rb'
+    - 'spec/presenters/commit_status_presenter_spec.rb'
+    - 'spec/presenters/projects/security/configuration_presenter_spec.rb'
+    - 'spec/requests/api/import_github_spec.rb'
+    - 'spec/support/shared_examples/services/jira/requests/base_shared_examples.rb'
+    - 'spec/views/user_settings/profiles/show.html.haml_spec.rb'
diff --git a/rubocop/cop/gitlab/documentation_links/link.rb b/rubocop/cop/gitlab/documentation_links/link.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a5a26b27d83c202749bd02bd0a8d7facab925e6d
--- /dev/null
+++ b/rubocop/cop/gitlab/documentation_links/link.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module RuboCop
+  module Cop
+    module Gitlab
+      module DocumentationLinks
+        # Ensure that `help_page_path` links to existing documentation and that the paths
+        # include the .md extension.
+        #
+        # @example
+        #
+        #   # bad
+        #   help_page_path('this/file/does/not/exist.md')
+        #   help_page_path('this/file/exists.md#but-not-this-anchor')
+        #   help_page_path('this/file/exists.md', anchor: 'but-not-this-anchor')
+        #   help_page_path(path_as_a_variable)
+        #   help_page_path('this/file/exists.md', anchor: anchor_as_a_variable)
+        #   help_page_path('this/file/exists')
+        #   help_page_path('this/file/exists.html')
+
+        #   # good
+        #   help_page_path('this/file/exists.md')
+        #   help_page_path('this/file/exists.md#and-this-anchor-too')
+        #   help_page_path('this/file/exists.md', anchor: 'and-this-anchor-too')
+        class Link < RuboCop::Cop::Base
+          extend RuboCop::Cop::AutoCorrector
+
+          MSG_PATH_NOT_A_STRING = '`help_page_path`\'s first argument must be passed as a string ' \
+            'so that Rubocop can ensure the linked file exists.'
+          MSG_PATH_NEEDS_MD_EXTENSION = 'Add .md extension to the link: %{path}.'
+          MSG_FILE_NOT_FOUND = 'This file does not exist: `%{file_path}`.'
+          MSG_ANCHOR_NOT_A_STRING = '`help_page_path`\'s `anchor` argument must be passed as a string ' \
+            'so that Rubocop can ensure it exists within the linked file.'
+          MSG_ANCHOR_NOT_FOUND = 'The anchor `#%{anchor}` was not found in `%{file_path}`.'
+
+          HEADER_ID = /(?:[ \t]+\{\#([A-Za-z][\w:-]*)\})?/
+          ATX_HEADER_MATCH = /^(\#{1,6})(.+?(?:\\#)?)\s*?#*#{HEADER_ID}\s*?\n/
+          NON_WORD_RE = /[^\p{Word}\- \t]/
+          MARKDOWN_LINK_TEXT = /\[(?<link_text>[^\]]+)\]\((?<link_url>[^)]+)\)/
+
+          class << self
+            attr_accessor :anchors_by_docs_file
+          end
+          self.anchors_by_docs_file = {}
+
+          def_node_matcher :help_page_path?, <<~PATTERN
+            (send _ {:help_page_url :help_page_path} $...)
+          PATTERN
+          RESTRICT_ON_SEND = %i[help_page_url help_page_path].to_set.freeze
+
+          def_node_matcher :anchor_param, <<~PATTERN
+            (send nil? %RESTRICT_ON_SEND
+              _
+              (hash
+                <(pair (sym :anchor) $_) ...>
+              )
+            )
+          PATTERN
+
+          def on_send(node)
+            return unless valid_argument_count?(node)
+            return unless first_argument_is_string?(node)
+
+            path = node.arguments.first.value
+            path_without_anchor = path.gsub(%r{#.*$}, '')
+
+            return unless path_has_md_extension?(node, path, path_without_anchor)
+
+            docs_file_path = File.join('doc', path_without_anchor)
+
+            return unless docs_file_exists?(node, docs_file_path)
+
+            anchor_exists_in_markdown?(node, docs_file_path)
+          end
+
+          def external_dependency_checksum
+            @external_dependency_checksum ||=
+              begin
+                mds = Dir["doc/**/*.md"]
+                digest = Digest::SHA512.new
+                mds.each { |md| digest.file(md) }
+                digest.hexdigest
+              end
+          end
+
+          private
+
+          def valid_argument_count?(node)
+            node.arguments.count > 0
+          end
+
+          def first_argument_is_string?(node)
+            return true if node.arguments.first.str_type?
+
+            add_offense(node, message: MSG_PATH_NOT_A_STRING)
+
+            false
+          end
+
+          def path_has_md_extension?(node, path, path_without_anchor)
+            return true if path_without_anchor.end_with?('.md')
+
+            add_offense(node, message: format(MSG_PATH_NEEDS_MD_EXTENSION, path: path)) do |corrector|
+              extension_pattern = /(\.[\da-zA-Z]+)?/
+              path_without_extension = path_without_anchor.gsub(/#{extension_pattern}$/, '')
+              arg_with_md_extension = path.gsub(/#{path_without_extension}#{extension_pattern}(\#.+)?$/,
+                "#{path_without_extension}.md\\2")
+              corrector.replace(node.arguments.first.source_range, "'#{arg_with_md_extension}'")
+            end
+
+            false
+          end
+
+          def docs_file_exists?(node, docs_file_path)
+            return true if File.exist?(docs_file_path)
+
+            add_offense(node, message: format(MSG_FILE_NOT_FOUND, file_path: docs_file_path))
+
+            false
+          end
+
+          def anchor_exists_in_markdown?(node, docs_file_path)
+            anchor = get_anchor(node)
+
+            return true unless anchor
+
+            anchors = get_anchors_in_markdown(docs_file_path)
+
+            return true if anchors.include?(anchor)
+
+            add_offense(node, message: format(MSG_ANCHOR_NOT_FOUND, anchor: anchor, file_path: docs_file_path))
+
+            false
+          end
+
+          def get_anchor(node)
+            return node.arguments.first.value[/#(.+)$/, 1] if node.arguments.length === 1
+
+            anchor_node = anchor_param(node)
+            return unless anchor_node
+            return anchor_node.value if anchor_node.str_type?
+
+            add_offense(node, message: MSG_ANCHOR_NOT_A_STRING)
+          end
+
+          # This methods extracts anchors from a Markdown file. The logic in here replicates our
+          # custom Kramdown header parser at https://gitlab.com/gitlab-org/ruby/gems/gitlab_kramdown/-/blob/bbc5ac439a2e6af60cbcce9a157283b2c5b59b38/lib/gitlab_kramdown/parser/header.rb.
+          # The logic is documented here: https://docs.gitlab.com/ee/user/markdown.html#heading-ids-and-links.
+          # There a special undocumnented syntax that makes it possible to set custom IDs, eg:
+          # ```md
+          # ### My heading {#my-custom-id}
+          # ```
+          # This would result in a `my-custom-id` anchor instead of `my-heading`. We are also handling
+          # this special syntax in here.
+          def get_anchors_in_markdown(docs_file_path)
+            self.class.anchors_by_docs_file.fetch(docs_file_path) do
+              docs_content = File.read(docs_file_path)
+              headers = docs_content.scan(ATX_HEADER_MATCH)
+              counters = Hash.new(0)
+
+              self.class.anchors_by_docs_file[docs_file_path] = headers.map do |header|
+                _level, text, id = header
+
+                id || generate_anchor(text, counters)
+              end
+            end
+          end
+
+          def generate_anchor(text, counters)
+            anchor = text.to_s.strip.downcase
+            anchor.gsub!(MARKDOWN_LINK_TEXT) { |s| MARKDOWN_LINK_TEXT.match(s)[:link_text].gsub(NON_WORD_RE, '') }
+            anchor.gsub!(NON_WORD_RE, '')
+            anchor.tr!(" \t", '-')
+            anchor << (counters[anchor] > 0 ? "-#{counters[anchor]}" : '')
+            counters[anchor] += 1
+            anchor
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/rubocop/rubocop-documentation.yml b/rubocop/rubocop-documentation.yml
new file mode 100644
index 0000000000000000000000000000000000000000..efe6d1853fa20a20c89cb06689704d9b3da6d3f8
--- /dev/null
+++ b/rubocop/rubocop-documentation.yml
@@ -0,0 +1,8 @@
+# Lints against outdated documentation links rendered in the product and enforces extension-less
+# paths.
+# For now, we disable the cop on all HAML files to not conflict with the `DocumentationLinks` HAML
+# linter. We'll eventually consolidate both cops into `Gitlab/DocumentationLinks/Link`.
+Gitlab/DocumentationLinks/Link:
+  Exclude:
+    - app/views/**/*.haml
+    - ee/app/views/**/*.haml
diff --git a/spec/rubocop/cop/gitlab/documentation_links/link_spec.rb b/spec/rubocop/cop/gitlab/documentation_links/link_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee7ea39d8d6fd89a22fc337a376d52f04ebbfe64
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/documentation_links/link_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../../rubocop/cop/gitlab/documentation_links/link'
+
+RSpec.describe RuboCop::Cop::Gitlab::DocumentationLinks::Link, feature_category: :navigation do
+  using RSpec::Parameterized::TableSyntax
+
+  shared_examples 'no offenses registered' do
+    it 'does not register any offenses' do
+      expect_no_offenses(code)
+    end
+  end
+
+  shared_examples 'offense registered' do |offense_message|
+    it 'registers an offense' do
+      expect_offense(<<~'RUBY', code: code, offense_message: offense_message)
+          %{code}
+          ^{code} %{offense_message}
+      RUBY
+    end
+  end
+
+  context 'when no argument is passed' do
+    let(:code) { "help_page_path" }
+
+    it_behaves_like 'no offenses registered'
+  end
+
+  context 'when the path is valid' do
+    before do
+      allow(File).to receive(:exist?).with('doc/this/file/exists.md').and_return(true)
+    end
+
+    where(:code) do
+      [
+        "help_page_path('/this/file/exists.md')",
+        "help_page_url('/this/file/exists.md')"
+      ]
+    end
+
+    with_them do
+      it_behaves_like 'no offenses registered'
+    end
+
+    describe 'anchors' do
+      before do
+        allow(File).to receive(:read).and_call_original
+        allow(File).to receive(:read).with('doc/this/file/exists.md').and_return(<<~MARKDOWN)
+          # Primary heading
+
+          Intro
+
+          ## This anchor exists
+
+          Content
+
+          ## This anchor exists
+
+          More content
+
+          ## This one has a custom ID {#my-custom-id}
+        MARKDOWN
+      end
+
+      context 'when the anchor is valid' do
+        where(:code) do
+          [
+            "help_page_path('/this/file/exists.md#primary-heading')",
+            "help_page_path('/this/file/exists.md#this-anchor-exists')",
+            "help_page_path('/this/file/exists.md#this-anchor-exists-1')",
+            "help_page_path('/this/file/exists.md', anchor: 'this-anchor-exists')",
+            "help_page_path('/this/file/exists.md', anchor: 'this-anchor-exists-1')",
+            "help_page_path('/this/file/exists.md', anchor: 'my-custom-id')",
+            "help_page_url('/this/file/exists.md#primary-heading')"
+          ]
+        end
+
+        with_them do
+          it_behaves_like 'no offenses registered'
+        end
+      end
+
+      context 'when the anchor is invalid' do
+        where(:code) do
+          [
+            "help_page_path('/this/file/exists.md#this-anchor-does-not-exist')",
+            "help_page_path('/this/file/exists.md', anchor: 'this-anchor-does-not-exist')",
+            "help_page_url('/this/file/exists.md#this-anchor-does-not-exist')"
+          ]
+        end
+
+        with_them do
+          it_behaves_like 'offense registered', "The anchor `#this-anchor-does-not-exist` was not found in [...]"
+        end
+      end
+
+      context 'when the anchor is not a string' do
+        let(:code) { "help_page_path('/this/file/exists.md', anchor: anchor_variable)" }
+
+        it_behaves_like 'offense registered', "`help_page_path`'s `anchor` argument must be passed as a string [...]"
+      end
+    end
+  end
+
+  context 'when the path is invalid' do
+    before do
+      allow(File).to receive(:exists).and_return(false)
+    end
+
+    where(:code) do
+      [
+        "help_page_path('/this/file/does/not/exist.md')",
+        "help_page_path('/this/file/does/not/exist.md#some-anchor')",
+        "help_page_path('/this/file/does/not/exist.md', anchor: 'some-anchor')",
+        "help_page_url('/this/file/does/not/exist.md')"
+      ]
+    end
+
+    with_them do
+      it_behaves_like 'offense registered', "This file does not exist: [...]"
+    end
+
+    context 'when the path is not a string' do
+      let(:code) { "help_page_path(path_variable)" }
+
+      it_behaves_like 'offense registered', "`help_page_path`'s first argument must be passed as a string [...]"
+    end
+
+    context 'when the path does not include the .md file extension' do
+      where(:path, :correction) do
+        '/this/path/lacks/md/extension'             | '/this/path/lacks/md/extension.md'
+        '/this/path/lacks/md/extension.html'        | '/this/path/lacks/md/extension.md'
+        '/this/path/lacks/md/extension#anchor'      | '/this/path/lacks/md/extension.md#anchor'
+        '/this/path/lacks/md/extension.html#anchor' | '/this/path/lacks/md/extension.md#anchor'
+      end
+
+      with_them do
+        it 'registers an offense and corrects' do
+          expect_offense(<<~'RUBY', code: "help_page_path('#{path}')")
+            %{code}
+            ^{code} Add .md extension to the link: [...]
+          RUBY
+
+          expect_correction("help_page_path('#{correction}')\n")
+        end
+      end
+    end
+  end
+
+  describe '#external_dependency_checksum' do
+    it 'returns a SHA256 digest used by RuboCop to invalid cache' do
+      expect(cop.external_dependency_checksum).to match(/^\h{128}$/)
+    end
+  end
+end