diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e4a5bc1ad80080ac7b6667bab348fadd14b6bdfd..46c6a3bd1e0ac61936a44bb3fea1ff0d51f7835d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -269,8 +269,8 @@ jobs:
       - name: Lint fenced code blocks
         run: yarn -s doc-fence-check
 
-      - name: Lint website docs
-        run: yarn -s lint-website-docs
+      - name: Lint documentation
+        run: yarn -s lint-documentation
 
   lint-other:
     needs: [setup]
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 027f00952aab5701d730f7dc1bb43e63e7519136..99cf44b61750dca1dfad168cbe7cdd5ab011468f 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -396,9 +396,6 @@ export interface RenovateOptionBase {
     | 'postUpgradeTasks'
     | 'regexManagers';
 
-  // used by tests
-  relatedOptions?: string[];
-
   stage?: RenovateConfigStage;
 
   experimental?: boolean;
diff --git a/package.json b/package.json
index 7f6e80d82b690dfba4677acb171cebe6760e34c4..d9fb273a830d925f1dbf00e843ede37a7e5d4c58 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
     "debug": "cross-env NODE_OPTIONS=--inspect-brk ts-node lib/renovate.ts",
     "doc-fix": "run-s markdown-lint-fix prettier-fix",
     "doc-fence-check": "node tools/check-fenced-code.mjs",
-    "lint-website-docs": "jest --coverage false test/website-docs.spec.ts",
+    "lint-documentation": "jest --coverage false test/documentation.spec.ts",
     "eslint": "eslint . --cache --cache-location .cache/eslint --report-unused-disable-directives",
     "eslint-fix": "eslint --cache --cache-location .cache/eslint --fix . --report-unused-disable-directives",
     "eslint-ci": "eslint . --cache --cache-strategy content --cache-location .cache/eslint --format gha",
diff --git a/test/documentation.spec.ts b/test/documentation.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da63620d84aad3ad94f42a9afcec08d15c1f690c
--- /dev/null
+++ b/test/documentation.spec.ts
@@ -0,0 +1,88 @@
+import fs from 'fs-extra';
+import { glob } from 'glob';
+import { getOptions } from '../lib/config/options';
+import { regEx } from '../lib/util/regex';
+
+const options = getOptions();
+const markdownGlob = '{docs,lib}/**/*.md';
+
+describe('documentation', () => {
+  it('has no invalid links', async () => {
+    const markdownFiles = await glob(markdownGlob);
+
+    await Promise.all(
+      markdownFiles.map(async (markdownFile) => {
+        const markdownText = await fs.readFile(markdownFile, 'utf8');
+        expect(markdownText).not.toMatch(regEx(/\.md\/#/));
+      })
+    );
+  });
+
+  describe('website-documentation', () => {
+    describe('configuration-options', () => {
+      const doc = fs.readFileSync(
+        'docs/usage/configuration-options.md',
+        'utf8'
+      );
+
+      const headers = doc
+        .match(/\n## (.*?)\n/g)
+        ?.map((match) => match.substring(4, match.length - 1));
+
+      const expectedOptions = options
+        .filter((option) => !option.globalOnly)
+        .filter((option) => !option.parent)
+        .filter((option) => !option.autogenerated)
+        .map((option) => option.name)
+        .sort();
+
+      it('has doc headers sorted alphabetically', () => {
+        expect(headers).toEqual([...headers!].sort());
+      });
+
+      it('has headers for every required option', () => {
+        expect(headers).toEqual(expectedOptions);
+      });
+
+      const subHeaders = doc
+        .match(/\n### (.*?)\n/g)
+        ?.map((match) => match.substring(5, match.length - 1));
+      subHeaders!.sort();
+      const expectedSubOptions = options
+        .filter((option) => option.stage !== 'global')
+        .filter((option) => !option.globalOnly)
+        .filter((option) => option.parent)
+        .map((option) => option.name)
+        .sort();
+      expectedSubOptions.sort();
+
+      it('has headers for every required sub-option', () => {
+        expect(subHeaders).toEqual(expectedSubOptions);
+      });
+    });
+
+    describe('self-hosted-configuration', () => {
+      const doc = fs.readFileSync(
+        'docs/usage/self-hosted-configuration.md',
+        'utf8'
+      );
+
+      const headers = doc
+        .match(/\n## (.*?)\n/g)
+        ?.map((match) => match.substring(4, match.length - 1));
+
+      const expectedOptions = options
+        .filter((option) => !!option.globalOnly)
+        .map((option) => option.name)
+        .sort();
+
+      it('has headers sorted alphabetically', () => {
+        expect(headers).toEqual([...headers!].sort());
+      });
+
+      it('has headers for every required option', () => {
+        expect(headers).toEqual(expectedOptions);
+      });
+    });
+  });
+});
diff --git a/test/website-docs.spec.ts b/test/website-docs.spec.ts
deleted file mode 100644
index 164a526992d288884c26fb10561eef4b2231127f..0000000000000000000000000000000000000000
--- a/test/website-docs.spec.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
-import fs from 'node:fs';
-import is from '@sindresorhus/is';
-import { getOptions } from '../lib/config/options';
-
-declare global {
-  // eslint-disable-next-line @typescript-eslint/no-namespace
-  namespace jest {
-    type ContainsOption<T> = T extends ArrayLike<unknown> ? T[number] : unknown;
-
-    interface Matchers<R> {
-      /**
-       * only available in `test/website-docs.spec.js`
-       * @param arg Value which current values should contain
-       */
-      toContainOption(arg: ContainsOption<R>): void;
-    }
-  }
-}
-
-const options = getOptions();
-
-describe('website-docs', () => {
-  const doc = fs.readFileSync('docs/usage/configuration-options.md', 'utf8');
-  const selfHostDoc = fs.readFileSync(
-    'docs/usage/self-hosted-configuration.md',
-    'utf8'
-  );
-  const headers = doc
-    .match(/\n## (.*?)\n/g)
-    ?.map((match) => match.substring(4, match.length - 1));
-  const selfHostHeaders = selfHostDoc
-    .match(/\n## (.*?)\n/g)
-    ?.map((match) => match.substring(4, match.length - 1));
-  const expectedOptions = options
-    .filter((option) => !option.globalOnly)
-    .filter((option) => !option.parent)
-    .filter((option) => !option.autogenerated)
-    .map((option) => option.name)
-    .sort();
-
-  const selfHostExpectedOptions = options
-    .filter((option) => !!option.globalOnly)
-    .map((option) => option.name)
-    .sort();
-
-  it('has doc headers sorted alphabetically', () => {
-    expect(headers).toEqual([...headers!].sort());
-  });
-
-  it('has headers for every required option', () => {
-    expect(headers).toEqual(expectedOptions);
-  });
-
-  it('has self hosted doc headers sorted alphabetically', () => {
-    expect(selfHostHeaders).toEqual([...selfHostHeaders!].sort());
-  });
-
-  it('has headers (self hosted) for every required option', () => {
-    expect(selfHostHeaders).toEqual(selfHostExpectedOptions);
-  });
-
-  const headers3 = doc
-    .match(/\n### (.*?)\n/g)
-    ?.map((match) => match.substring(5, match.length - 1));
-  headers3!.sort();
-  const expectedOptions3 = options
-    .filter((option) => option.stage !== 'global')
-    .filter((option) => !option.globalOnly)
-    .filter((option) => option.parent)
-    .map((option) => option.name)
-    .sort();
-  expectedOptions3.sort();
-
-  it('has headers for every required sub-option', () => {
-    expect(headers3).toEqual(expectedOptions3);
-  });
-
-  // Checking relatedOptions field in options
-  const relatedOptionsMatrix = options
-    .map((option) => option.relatedOptions)
-    .filter(is.truthy)
-    .sort();
-
-  let relatedOptions = ([] as string[]).concat(...relatedOptionsMatrix!); // Converts the matrix to an 1D array
-  relatedOptions = [...new Set(relatedOptions)]; // Makes all options unique
-
-  /*
-    Matcher which checks if the argument is within the received array (or string)
-    on an error, it throws a custom message.
-  */
-  expect.extend({
-    toContainOption<T extends string>(received: T[], argument: T) {
-      if (received.includes(argument)) {
-        return {
-          message: (): string =>
-            `Option "${argument}" should be within options`,
-          pass: true,
-        };
-      }
-      return {
-        message: (): string =>
-          `Option "${argument}" doesn't exist within options`,
-        pass: false,
-      };
-    },
-  });
-
-  const allOptionNames = options.map((option) => option.name).sort();
-
-  // Lists through each option in the relatedOptions array to be able to locate the exact element which causes error, in case of one
-  it('has valid relateOptions values', () => {
-    relatedOptions.forEach((relOption) => {
-      expect(allOptionNames).toContainOption(relOption);
-    });
-  });
-});