diff --git a/.eslintignore b/.eslintignore
index 423462ef5c92cab16d89120204a0d46cbfaec69b..775881bd7e3a830ac10ed300fe7a1b66d6e8f790 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -16,3 +16,4 @@ coverage
 **/*.generated.ts
 /tools/dist
 /patches
+tmp/
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4e82a7201f944fdd5c26027c248f3d29e57f837d..57faf368cb4e9925478cc6d26c6fe3f2b002973c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -204,3 +204,9 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+      - name: Upload docs
+        uses: actions/upload-artifact@v2.3.0
+        with:
+          name: docs
+          path: tmp/docs
diff --git a/.prettierignore b/.prettierignore
index 8208517732120b85719798ffdc18698c47fe1893..286760af2ee8bd27d6b0fa46568b54b26399465a 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,3 +18,4 @@ bin/yarn*
 **/*.generated.ts
 /tools/dist
 /patches
+**/tmp/
diff --git a/.releaserc b/.releaserc
index 014655c3a4f5e9be34d14c7bb4831e61200d85e6..3645aad973ca92037543657b94431be80d2ba60c 100644
--- a/.releaserc
+++ b/.releaserc
@@ -5,14 +5,21 @@
     [
       "@semantic-release/github",
       {
-        "releasedLabels": false
+        "releasedLabels": false,
+        "assets": [
+          {
+            "path": "tmp/docs.tgz",
+            "label": "docs.tgz"
+          }
+        ]
       }
     ],
     [
       "@semantic-release/exec",
       {
         "verifyConditionsCmd": "run-s verify",
-        "publishCmd": "run-s \"release -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}"
+        "prepareCmd": "run-s \"release:prepare -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}",
+        "publishCmd": "run-s \"release:publish -- {@}\" -- --release=${nextRelease.version} --sha=${nextRelease.gitHead} --tag=${nextRelease.channel}"
       }
     ]
   ],
diff --git a/docs/usage/gitlab-bot-security.md b/docs/usage/gitlab-bot-security.md
index 4f7b26f41118e3dfbff96f360cbcb55624c9734c..f1e38c9111c53e7754f0f37b66e8762de25b3a19 100644
--- a/docs/usage/gitlab-bot-security.md
+++ b/docs/usage/gitlab-bot-security.md
@@ -1,3 +1,7 @@
+---
+title: GitLab bot security
+---
+
 # GitLab bot security
 
 You should understand GitLab's security model, before deciding to run a "bot" service like Renovate on GitLab, particularly the pipeline credentials.
diff --git a/docs/usage/modules/datasource.md b/docs/usage/modules/datasource.md
index fecf9a0a4bb654709eb94aca86b5ba14c8ef4cb2..4d33d70e60341d9ee9c345ef7e1fe58efe1ac5d8 100644
--- a/docs/usage/modules/datasource.md
+++ b/docs/usage/modules/datasource.md
@@ -1,3 +1,7 @@
+---
+title: Datasources
+---
+
 # Datasources
 
 Once Renovate's manager is done scanning files and extracting dependencies, it will assign a `datasource` to each extracted package file and/or dependency so that Renovate then knows how to search for new versions.
diff --git a/docs/usage/modules/manager.md b/docs/usage/modules/manager.md
index c91d3df65242f28124aacb2daa87af843357d43b..b8fc9be8d9cc12d3f08286f9cb7e01ae5ff11c1f 100644
--- a/docs/usage/modules/manager.md
+++ b/docs/usage/modules/manager.md
@@ -1,3 +1,7 @@
+---
+title: Managers
+---
+
 # Managers
 
 Renovate is based around the concept of "package managers", or "managers" for short.
diff --git a/docs/usage/modules/platform.md b/docs/usage/modules/platform.md
index 9459d4003945f91600a293e167218cb5b14a9b07..b96f915c72a15a0a3aefa6851d2a4eefd4d22612 100644
--- a/docs/usage/modules/platform.md
+++ b/docs/usage/modules/platform.md
@@ -1,3 +1,7 @@
+---
+title: Platforms
+---
+
 # Renovate Platforms
 
 Renovate aims to be platform-neutral, while also taking advantage of good platform-specific features.
diff --git a/docs/usage/modules/versioning.md b/docs/usage/modules/versioning.md
index df24d46c8e1dd5475e06cb6f7aaf80462c220c38..708116b5777ae5e97a50b3383589005191ac7f19 100644
--- a/docs/usage/modules/versioning.md
+++ b/docs/usage/modules/versioning.md
@@ -1,3 +1,7 @@
+---
+title: Versioning
+---
+
 # Versioning
 
 Once Managers have extracted dependencies, and Datasources have located available versions, then Renovate will use a "Versioning" scheme to perform sorting and filtering of results.
diff --git a/docs/usage/templates.md b/docs/usage/templates.md
index 470c1d0efda945b7a99dc0a2cd944bd4aa204f0b..de2ea87d46a6fadfa5a0e8d5d9c46432881e1136 100644
--- a/docs/usage/templates.md
+++ b/docs/usage/templates.md
@@ -15,10 +15,16 @@ Some are configuration options passed through, while others are generated as par
 
 ## Exposed config options
 
+<!-- Autogenerate in https://github.com/renovatebot/renovatebot.github.io -->
+<!-- Autogenerate end -->
+
 <!-- Automatically insert exposed configuration options here -->
 
 ## Other available fields
 
+<!-- Autogenerate in https://github.com/renovatebot/renovatebot.github.io -->
+<!-- Autogenerate end -->
+
 <!-- Insert runtime fields here -->
 
 ## Additional Handlebars helpers
diff --git a/lib/datasource/types.ts b/lib/datasource/types.ts
index c03502dde6a2c56f413e70f612f46612c6c72d97..2f803443fb7e9ba1887db1b536c8e1e2c9049de4 100644
--- a/lib/datasource/types.ts
+++ b/lib/datasource/types.ts
@@ -1,3 +1,5 @@
+import type { ModuleApi } from '../types';
+
 export interface Config {
   datasource?: string;
   depName?: string;
@@ -66,7 +68,7 @@ export interface ReleaseResult {
   replacementVersion?: string;
 }
 
-export interface DatasourceApi {
+export interface DatasourceApi extends ModuleApi {
   id: string;
   getDigest?(config: DigestConfig, newValue?: string): Promise<string | null>;
   getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null>;
@@ -93,4 +95,7 @@ export interface DatasourceApi {
    * false: caching is not performed, or performed within the datasource implementation
    */
   caching?: boolean;
+
+  /** optional URLs to add to docs as references */
+  urls?: string[];
 }
diff --git a/lib/manager/types.ts b/lib/manager/types.ts
index 72acef11ce2d4bddda29c70737999f705d4319cb..036ae7ce5611b27dd0c113e1cd8616b73e21acc8 100644
--- a/lib/manager/types.ts
+++ b/lib/manager/types.ts
@@ -5,7 +5,7 @@ import type {
   ValidationMessage,
 } from '../config/types';
 import type { ProgrammingLanguage } from '../constants';
-import type { RangeStrategy, SkipReason } from '../types';
+import type { ModuleApi, RangeStrategy, SkipReason } from '../types';
 import type { File } from '../util/git/types';
 
 export type Result<T> = T | Promise<T>;
@@ -235,7 +235,7 @@ export interface GlobalManagerConfig {
   npmrcMerge?: boolean;
 }
 
-export interface ManagerApi {
+export interface ManagerApi extends ModuleApi {
   defaultConfig: Record<string, unknown>;
   language?: ProgrammingLanguage;
   supportsLockFileMaintenance?: boolean;
diff --git a/lib/types/base.ts b/lib/types/base.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e55530ca3f8d28b470830c9cd3fb28494f42fc97
--- /dev/null
+++ b/lib/types/base.ts
@@ -0,0 +1,4 @@
+export interface ModuleApi {
+  displayName?: string;
+  url?: string;
+}
diff --git a/lib/types/index.ts b/lib/types/index.ts
index e1e0fa9d4271991258fb868c06e04e77c5541fe6..f85b78de349197ab443ef2cb922c42931ec8c109 100644
--- a/lib/types/index.ts
+++ b/lib/types/index.ts
@@ -4,3 +4,4 @@ export * from './versioning';
 export * from './branch-status';
 export * from './vulnerability-alert';
 export * from './pr-state';
+export * from './base';
diff --git a/package.json b/package.json
index 90074825c2395d0c080485a0463b801663eb907d..12cd683a37938f63be501123d78dc07af489f828 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
   },
   "scripts": {
     "build": "run-s clean generate:* compile:*",
-    "clean": "rimraf dist",
+    "build:docs": "run-s \"release:prepare {@}\"",
+    "clean": "rimraf dist tmp",
     "clean-cache": "node bin/clean-cache.js",
     "compile:ts": "tsc -p tsconfig.app.json",
     "config-validator": "node -r ts-node/register/transpile-only -- lib/config-validator.ts",
@@ -38,7 +39,8 @@
     "pretest": "run-s generate:* ",
     "prettier": "prettier --check \"**/*.{ts,js,mjs,json,md,yml}\"",
     "prettier-fix": "prettier --write \"**/*.{ts,js,mjs,json,md,yml}\"",
-    "release": "node tools/release.mjs",
+    "release:prepare": "node -r ts-node/register/transpile-only -- tools/generate-docs.ts",
+    "release:publish": "node tools/release.mjs",
     "start": "node -r ts-node/register/transpile-only -- lib/renovate.ts",
     "test": "run-s lint test-schema type-check null-check jest",
     "test-dirty": "git diff --exit-code",
diff --git a/tools/docs/config.ts b/tools/docs/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a06fa355b536d369091efb67852f1e10031f1e23
--- /dev/null
+++ b/tools/docs/config.ts
@@ -0,0 +1,91 @@
+import table from 'markdown-table';
+import { getOptions } from '../../lib/config/options';
+import { getCliName } from '../../lib/workers/global/config/parse/cli';
+import { getEnvName } from '../../lib/workers/global/config/parse/env';
+import { readFile, updateFile } from '../utils/index';
+
+const options = getOptions();
+
+function genTable(obj: [string, string][], type: string, def: any): string {
+  const data = [['Name', 'Value']];
+  const name = obj[0][1];
+  const ignoredKeys = [
+    'name',
+    'description',
+    'default',
+    'stage',
+    'allowString',
+    'cli',
+    'env',
+    'admin',
+  ];
+  obj.forEach(([key, val]) => {
+    const el = [key, val];
+    if (
+      !ignoredKeys.includes(el[0]) ||
+      (el[0] === 'default' && typeof el[1] !== 'object' && name !== 'prBody')
+    ) {
+      if (type === 'string' && el[0] === 'default') {
+        el[1] = `\`"${el[1]}"\``;
+      }
+      if (type === 'boolean' && el[0] === 'default') {
+        el[1] = `\`${el[1]}\``;
+      }
+      if (type === 'string' && el[0] === 'default' && el[1].length > 200) {
+        el[1] = `[template]`;
+      }
+      data.push(el);
+    }
+  });
+
+  if (type === 'list') {
+    data.push(['default', '`[]`']);
+  }
+  if (type === 'string' && def === undefined) {
+    data.push(['default', '`null`']);
+  }
+  if (type === 'boolean' && def === undefined) {
+    data.push(['default', '`true`']);
+  }
+  if (type === 'boolean' && def === null) {
+    data.push(['default', '`null`']);
+  }
+  return table(data);
+}
+
+export async function generateConfig(dist: string, bot = false): Promise<void> {
+  let configFile = `configuration-options.md`;
+  if (bot) {
+    configFile = `self-hosted-configuration.md`;
+  }
+
+  const configOptionsRaw = (await readFile(`docs/usage/${configFile}`)).split(
+    '\n'
+  );
+
+  options
+    .filter((option) => option.releaseStatus !== 'unpublished')
+    .forEach((option) => {
+      const el: Record<string, any> = { ...option };
+      let headerIndex = configOptionsRaw.indexOf(`## ${option.name}`);
+      if (headerIndex === -1) {
+        headerIndex = configOptionsRaw.indexOf(`### ${option.name}`);
+      }
+      if (bot) {
+        el.cli = getCliName(el);
+        el.env = getEnvName(el);
+        if (el.cli === '') {
+          el.cli = `N/A`;
+        }
+        if (el.env === '') {
+          el.env = 'N/A';
+        }
+      }
+
+      configOptionsRaw[headerIndex] +=
+        `\n${option.description}\n\n` +
+        genTable(Object.entries(el), option.type, option.default);
+    });
+
+  await updateFile(`${dist}/${configFile}`, configOptionsRaw.join('\n'));
+}
diff --git a/tools/docs/datasources.ts b/tools/docs/datasources.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9589863a17b7188f797a7e3dda1df5bc489e651c
--- /dev/null
+++ b/tools/docs/datasources.ts
@@ -0,0 +1,36 @@
+import { getDatasources } from '../../lib/datasource';
+import { readFile, updateFile } from '../utils';
+import {
+  formatDescription,
+  formatUrls,
+  getDisplayName,
+  replaceContent,
+} from './utils';
+
+export async function generateDatasources(dist: string): Promise<void> {
+  const dsList = getDatasources();
+  let datasourceContent =
+    '\nSupported values for `datasource` are: ' +
+    [...dsList.keys()].map((v) => `\`${v}\``).join(', ') +
+    '.\n\n';
+  for (const [datasource, definition] of dsList) {
+    const { id, urls, defaultConfig } = definition;
+    const displayName = getDisplayName(datasource, definition);
+    datasourceContent += `\n### ${displayName} Datasource\n\n`;
+    datasourceContent += `**Identifier**: \`${id}\`\n\n`;
+    datasourceContent += formatUrls(urls);
+    datasourceContent += await formatDescription('datasource', datasource);
+
+    if (defaultConfig) {
+      datasourceContent +=
+        '**Default configuration**:\n\n```json\n' +
+        JSON.stringify(defaultConfig, undefined, 2) +
+        '\n```\n';
+    }
+
+    datasourceContent += `\n----\n\n`;
+  }
+  let indexContent = await readFile(`docs/usage/modules/datasource.md`);
+  indexContent = replaceContent(indexContent, datasourceContent);
+  await updateFile(`${dist}/modules/datasource.md`, indexContent);
+}
diff --git a/tools/docs/manager.ts b/tools/docs/manager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf3bbd4323f2daa98726d05aed2a9454acedd88d
--- /dev/null
+++ b/tools/docs/manager.ts
@@ -0,0 +1,86 @@
+import type { RenovateConfig } from '../../lib/config/types';
+import { getManagers } from '../../lib/manager';
+import { readFile, updateFile } from '../utils';
+import { getDisplayName, getNameWithUrl, replaceContent } from './utils';
+
+function getTitle(manager: string, displayName: string): string {
+  if (manager === 'regex') {
+    return `Custom Manager Support using Regex`;
+  }
+  return `Automated Dependency Updates for ${displayName}`;
+}
+
+function getManagerLink(manager: string): string {
+  return `[\`${manager}\`](${manager}/)`;
+}
+
+export async function generateManagers(dist: string): Promise<void> {
+  const managers = getManagers();
+  const allLanguages: Record<string, string[]> = {};
+  for (const [manager, definition] of managers) {
+    const language = definition.language || 'other';
+    allLanguages[language] = allLanguages[language] || [];
+    allLanguages[language].push(manager);
+    const { defaultConfig } = definition;
+    const { fileMatch } = defaultConfig as RenovateConfig;
+    const displayName = getDisplayName(manager, definition);
+    let md = `---
+title: ${getTitle(manager, displayName)}
+sidebar_label: ${displayName}
+---
+`;
+    if (manager !== 'regex') {
+      const nameWithUrl = getNameWithUrl(manager, definition);
+      md += `Renovate supports updating ${nameWithUrl} dependencies.\n\n`;
+      if (defaultConfig.enabled === false) {
+        md += '## Enabling\n\n';
+        md += `${displayName} functionality is currently in beta testing so you must opt in to test it out. To enable it, add a configuration like this to either your bot config or your \`renovate.json\`:\n\n`;
+        md += '```\n';
+        md += `{\n  "${manager}": {\n    "enabled": true\n  }\n}`;
+        md += '\n```\n\n';
+        md +=
+          'If you encounter any bugs, please [raise a bug report](https://github.com/renovatebot/renovate/issues/new?template=3-Bug_report.md). If you find that it works well, then feedback on that would be welcome too.\n\n';
+      }
+      md += '## File Matching\n\n';
+      if (!Array.isArray(fileMatch) || fileMatch.length === 0) {
+        md += `Because file names for \`${manager}\` cannot be easily determined automatically, Renovate will not attempt to match any \`${manager}\` files by default. `;
+      } else {
+        md += `By default, Renovate will check any files matching `;
+        if (fileMatch.length === 1) {
+          md += `the following regular expression: \`${fileMatch[0]}\`.\n\n`;
+        } else {
+          md += `any of the following regular expressions:\n\n`;
+          md += '```\n';
+          md += fileMatch.join('\n');
+          md += '\n```\n\n';
+        }
+      }
+      md += `For details on how to extend a manager's \`fileMatch\` value, please follow [this link](/modules/manager/#file-matching).\n\n`;
+    }
+
+    const managerReadmeContent = await readFile(
+      `lib/manager/${manager}/readme.md`
+    );
+    if (manager !== 'regex') {
+      md += '\n## Additional Information\n\n';
+    }
+    md += managerReadmeContent + '\n\n';
+
+    await updateFile(`${dist}/modules/manager/${manager}/index.md`, md);
+  }
+  const languages = Object.keys(allLanguages).filter(
+    (language) => language !== 'other'
+  );
+  languages.sort();
+  languages.push('other');
+  let languageText = '\n';
+
+  for (const language of languages) {
+    languageText += `**${language}**: `;
+    languageText += allLanguages[language].map(getManagerLink).join(', ');
+    languageText += '\n\n';
+  }
+  let indexContent = await readFile(`docs/usage/modules/manager.md`);
+  indexContent = replaceContent(indexContent, languageText);
+  await updateFile(`${dist}/modules/manager.md`, indexContent);
+}
diff --git a/tools/docs/modules.ts b/tools/docs/modules.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc15d58d81eda9fc043361b14c0f13e4f7799719
--- /dev/null
+++ b/tools/docs/modules.ts
@@ -0,0 +1,26 @@
+// import shell from 'shelljs';
+
+// import { readFile, updateFile } from '../utils/index.js';
+// // import { getDisplayName, getNameWithUrl, replaceContent } from './utils.js';
+
+// const fileRe = /modules[/\\](.+?)\.md$/;
+// const titleRe = /^#(.+?)$/m;
+
+export async function generateModules(_root: string): Promise<void> {
+  //   for (const f of shell.ls('../usage/modules/*.md')) {
+  //     const [, tgt] = fileRe.exec(f);
+
+  //     let content = await readFile(f);
+  //     content = content.replace(
+  //       titleRe,
+  //       `---
+  // title: $1
+  // ---
+  // `
+  //     );
+
+  //     await updateFile(`./docs/modules/${tgt}.md`, content);
+  //   }
+
+  await Promise.resolve();
+}
diff --git a/tools/docs/platforms.ts b/tools/docs/platforms.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7bfd1ee0066642b9e1a46ea9c6e9406246d8ae02
--- /dev/null
+++ b/tools/docs/platforms.ts
@@ -0,0 +1,27 @@
+import { getPlatformList } from '../../lib/platform';
+import { readFile, updateFile } from '../utils';
+import { replaceContent } from './utils';
+
+function getModuleLink(module: string, title: string): string {
+  return `[${title ?? module}](${module}/)`;
+}
+
+export async function generatePlatforms(dist: string): Promise<void> {
+  let platformContent = 'Supported values for `platform` are: ';
+  const platforms = getPlatformList();
+  for (const platform of platforms) {
+    const readme = await readFile(`lib/platform/${platform}/index.md`);
+    await updateFile(`${dist}/modules/platform/${platform}/index.md`, readme);
+  }
+
+  platformContent += platforms
+    .map((v) => getModuleLink(v, `\`${v}\``))
+    .join(', ');
+
+  platformContent += '.\n';
+
+  const indexFileName = `docs/usage/modules/platform.md`;
+  let indexContent = await readFile(indexFileName);
+  indexContent = replaceContent(indexContent, platformContent);
+  await updateFile(`${dist}/modules/platform.md`, indexContent);
+}
diff --git a/tools/docs/presets.ts b/tools/docs/presets.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dfa7e67781666a6fc922eebebcc8302510a1a159
--- /dev/null
+++ b/tools/docs/presets.ts
@@ -0,0 +1,70 @@
+import { groups as presetGroups } from '../../lib/config/presets/internal';
+import { logger } from '../../lib/logger';
+import { updateFile } from '../utils';
+
+function jsUcfirst(string: string): string {
+  return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+/**
+ * @param {string} name
+ * @param {number} order
+ */
+function generateFrontMatter(name: string, order: number): string {
+  return `---
+date: 2017-12-07
+title: ${name} Presets
+categories:
+    - config-presets
+type: Document
+order: ${order}
+---
+`;
+}
+
+export async function generatePresets(dist: string): Promise<void> {
+  let index = 0;
+  for (const [name, presetConfig] of Object.entries(presetGroups)) {
+    index += 1;
+    const formattedName = jsUcfirst(name)
+      .replace('Js', 'JS')
+      .replace(/s$/, '')
+      .replace(/^Config$/, 'Full Config');
+    const frontMatter = generateFrontMatter(formattedName, index);
+    let content = `\n`;
+    for (const [preset, value] of Object.entries(presetConfig)) {
+      let header = `\n### ${name === 'default' ? '' : name}:${preset}`;
+      let presetDescription = value.description;
+      delete value.description;
+      if (!presetDescription) {
+        if (value.packageRules?.[0].description) {
+          presetDescription = value.packageRules[0].description;
+          delete value.packageRules[0].description;
+        }
+      }
+      let body = '';
+      if (presetDescription) {
+        body += `\n\n${presetDescription}\n`;
+      } else {
+        logger.warn(`Preset ${name}:${preset} has no description`);
+      }
+      body += '\n```\n';
+      body += JSON.stringify(value, null, 2);
+      body += '\n```\n';
+      body += '----\n';
+      if (body.includes('{{arg0}}')) {
+        header += '(`<arg0>`';
+        if (body.includes('{{arg1}}')) {
+          header += ', `<arg1>`';
+          if (body.includes('{{arg2}}')) {
+            header += ', `<arg2>`';
+          }
+        }
+        header += ')';
+        body = body.replace(/{{(arg\d+)}}/g, '$1');
+      }
+      content += header + body;
+    }
+    await updateFile(`${dist}/presets-${name}.md`, frontMatter + content);
+  }
+}
diff --git a/tools/docs/schema.ts b/tools/docs/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8576e14d0fbfc2693b344d6fd158893df2936393
--- /dev/null
+++ b/tools/docs/schema.ts
@@ -0,0 +1,111 @@
+import { getOptions } from '../../lib/config/options';
+import type { RenovateOptions } from '../../lib/config/types';
+import { hasKey } from '../../lib/util/object';
+import { updateFile } from '../utils';
+
+const schema = {
+  title: 'JSON schema for Renovate config files (https://renovatebot.com/)',
+  $schema: 'http://json-schema.org/draft-04/schema#',
+  type: 'object',
+  properties: {},
+};
+const options = getOptions();
+options.sort((a, b) => {
+  if (a.name < b.name) {
+    return -1;
+  }
+  if (a.name > b.name) {
+    return 1;
+  }
+  return 0;
+});
+const properties = schema.properties as Record<string, any>;
+
+function createSingleConfig(option: RenovateOptions): Record<string, unknown> {
+  const temp = {} as Record<string, any> & RenovateOptions;
+  if (option.description) {
+    temp.description = option.description;
+  }
+  temp.type = option.type;
+  if (option.type === 'array') {
+    if (option.subType) {
+      temp.items = {
+        type: option.subType,
+      };
+      if (hasKey('format', option) && option.format) {
+        temp.items.format = option.format;
+      }
+      if (option.allowedValues) {
+        temp.items.enum = option.allowedValues;
+      }
+    }
+    if (option.subType === 'string' && option.allowString === true) {
+      const items = temp.items;
+      delete temp.items;
+      delete temp.type;
+      temp.oneOf = [{ type: 'array', items }, { ...items }];
+    }
+  } else {
+    if (hasKey('format', option) && option.format) {
+      temp.format = option.format;
+    }
+    if (option.allowedValues) {
+      temp.enum = option.allowedValues;
+    }
+  }
+  if (option.default !== undefined) {
+    temp.default = option.default;
+  }
+  if (
+    hasKey('additionalProperties', option) &&
+    option.additionalProperties !== undefined
+  ) {
+    temp.additionalProperties = option.additionalProperties;
+  }
+  if (temp.type === 'object' && !option.freeChoice) {
+    temp.$ref = '#';
+  }
+  return temp;
+}
+
+function createSchemaForParentConfigs(): void {
+  for (const option of options) {
+    if (!option.parent) {
+      properties[option.name] = createSingleConfig(option);
+    }
+  }
+}
+
+function addChildrenArrayInParents(): void {
+  for (const option of options) {
+    if (option.parent) {
+      properties[option.parent].items = {
+        allOf: [
+          {
+            type: 'object',
+            properties: {},
+          },
+        ],
+      };
+    }
+  }
+}
+
+function createSchemaForChildConfigs(): void {
+  for (const option of options) {
+    if (option.parent) {
+      properties[option.parent].items.allOf[0].properties[option.name] =
+        createSingleConfig(option);
+    }
+  }
+}
+
+export async function generateSchema(dist: string): Promise<void> {
+  createSchemaForParentConfigs();
+  addChildrenArrayInParents();
+  createSchemaForChildConfigs();
+  await updateFile(
+    `${dist}/renovate-schema.json`,
+    `${JSON.stringify(schema, null, 2)}\n`
+  );
+}
diff --git a/tools/docs/templates.ts b/tools/docs/templates.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1867c0f3986a6fbb54a7ac0cd3004bca5d23303e
--- /dev/null
+++ b/tools/docs/templates.ts
@@ -0,0 +1,26 @@
+import { allowedFields, exposedConfigOptions } from '../../lib/util/template';
+import { readFile, updateFile } from '../utils';
+import { replaceContent } from './utils';
+
+export async function generateTemplates(dist: string): Promise<void> {
+  let exposedConfigOptionsText =
+    'The following configuration options are passed through for templating: ';
+  exposedConfigOptionsText +=
+    exposedConfigOptions
+      .map(
+        (field) => `[${field}](/configuration-options/#${field.toLowerCase()})`
+      )
+      .join(', ') + '.';
+
+  let runtimeText =
+    'The following runtime values are passed through for templating: \n\n';
+  for (const [field, description] of Object.entries(allowedFields)) {
+    runtimeText += ` - \`${field}\`: ${description}\n`;
+  }
+  runtimeText += '\n\n';
+
+  let templateContent = await readFile('docs/usage/templates.md');
+  templateContent = replaceContent(templateContent, exposedConfigOptionsText);
+  templateContent = replaceContent(templateContent, runtimeText);
+  await updateFile(`${dist}/templates.md`, templateContent);
+}
diff --git a/tools/docs/utils.ts b/tools/docs/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c8c8877dfcdaee8832d23e9f48d43eaf19cd33d1
--- /dev/null
+++ b/tools/docs/utils.ts
@@ -0,0 +1,69 @@
+import { logger } from '../../lib/logger';
+import type { ModuleApi } from '../../lib/types';
+import { readFile } from '../utils';
+
+const replaceStart =
+  '<!-- Autogenerate in https://github.com/renovatebot/renovatebot.github.io -->';
+const replaceStop = '<!-- Autogenerate end -->';
+
+export function capitalize(input: string): string {
+  // console.log(input);
+  return input[0].toUpperCase() + input.slice(1);
+}
+
+export function formatName(input: string): string {
+  return input.split('-').map(capitalize).join(' ');
+}
+
+export function getDisplayName(
+  moduleName: string,
+  moduleDefinition: ModuleApi
+): string {
+  return moduleDefinition.displayName || formatName(moduleName);
+}
+
+export function getNameWithUrl(
+  moduleName: string,
+  moduleDefinition: ModuleApi
+): string {
+  const displayName = getDisplayName(moduleName, moduleDefinition);
+  if (moduleDefinition.url) {
+    return `[${displayName}](${moduleDefinition.url})`;
+  }
+  return displayName;
+}
+
+export function replaceContent(content: string, txt: string): string {
+  const replaceStartIndex = content.indexOf(replaceStart);
+  const replaceStopIndex = content.indexOf(replaceStop);
+
+  if (replaceStartIndex < 0) {
+    logger.error('Missing replace placeholder');
+    return content;
+  }
+  return (
+    content.slice(0, replaceStartIndex) +
+    txt +
+    content.slice(replaceStopIndex + replaceStop.length)
+  );
+}
+
+export function formatUrls(urls: string[] | null | undefined): string {
+  if (Array.isArray(urls) && urls.length) {
+    return `**References**:\n\n${urls
+      .map((url) => ` - [${url}](${url})`)
+      .join('\n')}\n\n`;
+  }
+  return '';
+}
+
+export async function formatDescription(
+  type: string,
+  name: string
+): Promise<string> {
+  const content = await readFile(`lib/${type}/${name}/readme.md`);
+  if (!content) {
+    return '';
+  }
+  return `**Description**:\n\n${content}\n`;
+}
diff --git a/tools/docs/versioning.ts b/tools/docs/versioning.ts
new file mode 100644
index 0000000000000000000000000000000000000000..780fa8c90c72b19a36713b6949345eccd6b5912f
--- /dev/null
+++ b/tools/docs/versioning.ts
@@ -0,0 +1,34 @@
+import { getVersioningList } from '../../lib/versioning';
+import { readFile, updateFile } from '../utils';
+import { formatDescription, formatUrls, replaceContent } from './utils';
+
+export async function generateVersioning(dist: string): Promise<void> {
+  const versioningList = getVersioningList();
+  let versioningContent =
+    '\nSupported values for `versioning` are: ' +
+    versioningList.map((v) => `\`${v}\``).join(', ') +
+    '.\n\n';
+  for (const versioning of versioningList) {
+    const definition = await import(`../../lib/versioning/${versioning}`);
+    const { id, displayName, urls, supportsRanges, supportedRangeStrategies } =
+      definition;
+    versioningContent += `\n### ${displayName} Versioning\n\n`;
+    versioningContent += `**Identifier**: \`${id}\`\n\n`;
+    versioningContent += formatUrls(urls);
+    versioningContent += `**Ranges/Constraints:**\n\n`;
+    if (supportsRanges) {
+      versioningContent += `✅ Ranges are supported.\n\nValid \`rangeStrategy\` values are: ${(
+        supportedRangeStrategies || []
+      )
+        .map((strategy: string) => `\`${strategy}\``)
+        .join(', ')}\n\n`;
+    } else {
+      versioningContent += `❌ No range support.\n\n`;
+    }
+    versioningContent += await formatDescription('versioning', versioning);
+    versioningContent += `\n----\n\n`;
+  }
+  let indexContent = await readFile(`docs/usage/modules/versioning.md`);
+  indexContent = replaceContent(indexContent, versioningContent);
+  await updateFile(`${dist}/modules/versioning.md`, indexContent);
+}
diff --git a/tools/generate-docs.ts b/tools/generate-docs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4e23ebcf747a806f9b2ca26b1bde92cdfef893fb
--- /dev/null
+++ b/tools/generate-docs.ts
@@ -0,0 +1,89 @@
+import { ERROR } from 'bunyan';
+import shell from 'shelljs';
+import { getProblems, logger } from '../lib/logger';
+import { generateConfig } from './docs/config';
+import { generateDatasources } from './docs/datasources';
+import { generateManagers } from './docs/manager';
+import { generateModules } from './docs/modules';
+import { generatePlatforms } from './docs/platforms';
+import { generatePresets } from './docs/presets';
+import { generateSchema } from './docs/schema';
+import { generateTemplates } from './docs/templates';
+import { generateVersioning } from './docs/versioning';
+
+process.on('unhandledRejection', (err) => {
+  // Will print "unhandledRejection err is not defined"
+  logger.error({ err }, 'unhandledRejection');
+  process.exit(-1);
+});
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+(async () => {
+  try {
+    const dist = 'tmp/docs';
+    let r: shell.ShellString;
+
+    logger.info('generating docs');
+
+    r = shell.mkdir('-p', `${dist}/`);
+    if (r.code) {
+      return;
+    }
+
+    logger.info('* static');
+    r = shell.cp('-r', 'docs/usage/*', `${dist}/`);
+    if (r.code) {
+      return;
+    }
+
+    logger.info('* modules');
+    await generateModules(dist);
+
+    logger.info('* platforms');
+    await generatePlatforms(dist);
+
+    // versionigs
+    logger.info('* versionigs');
+    await generateVersioning(dist);
+
+    // datasources
+    logger.info('* datasources');
+    await generateDatasources(dist);
+
+    // managers
+    logger.info('* managers');
+    await generateManagers(dist);
+
+    // presets
+    logger.info('* presets');
+    await generatePresets(dist);
+
+    // templates
+    logger.info('* templates');
+    await generateTemplates(dist);
+
+    // configuration-options
+    logger.info('* configuration-options');
+    await generateConfig(dist);
+
+    // self-hosted-configuration
+    logger.info('* self-hosted-configuration');
+    await generateConfig(dist, true);
+
+    // json-schema
+    logger.info('* json-schema');
+    await generateSchema(dist);
+
+    r = shell.exec('tar -czf ./tmp/docs.tgz -C ./tmp/docs .');
+    if (r.code) {
+      return;
+    }
+  } catch (err) {
+    logger.error({ err }, 'Unexpected error');
+  } finally {
+    const loggerErrors = getProblems().filter((p) => p.level >= ERROR);
+    if (loggerErrors.length) {
+      shell.exit(1);
+    }
+  }
+})();
diff --git a/tools/utils/index.ts b/tools/utils/index.ts
index ff4ef7fe74e6b7c493aa40253150fddef833452c..d17bf464ec047cbd5ac540544b6344234875df94 100644
--- a/tools/utils/index.ts
+++ b/tools/utils/index.ts
@@ -1,8 +1,67 @@
+import fs from 'fs-extra';
+import { logger } from '../../lib/logger';
+
+export const newFiles = new Set();
+
 /**
  * Get environment variable or empty string.
  * Used for easy mocking.
- * @param key variable name
+ * @param {string} key variable name
+ * @returns {string}
  */
 export function getEnv(key: string): string {
-  return process.env[key] ?? '';
+  return process.env[key] || '';
+}
+
+/**
+ * Find all module directories.
+ * @param {string} dirname dir to search in
+ * @returns {string[]}
+ */
+export function findModules(dirname: string): string[] {
+  return fs
+    .readdirSync(dirname, { withFileTypes: true })
+    .filter((dirent) => dirent.isDirectory())
+    .map((dirent) => dirent.name)
+    .filter((name) => !name.startsWith('__'))
+    .sort();
+}
+
+/**
+ * @param {string} input
+ * @returns {string}
+ */
+export function camelCase(input: string): string {
+  return input
+    .replace(/(?:^\w|[A-Z]|\b\w)/g, (char, index) =>
+      index === 0 ? char.toLowerCase() : char.toUpperCase()
+    )
+    .replace(/-/g, '');
+}
+
+/**
+ * @param {string } file
+ * @param {string} code
+ * @returns {Promise<void>}
+ */
+export async function updateFile(file: string, code: string): Promise<void> {
+  const oldCode = fs.existsSync(file) ? await fs.readFile(file, 'utf8') : null;
+  if (code !== oldCode) {
+    if (!code) {
+      logger.error({ file }, 'Missing content');
+    }
+    await fs.outputFile(file, code ?? '', { encoding: 'utf8' });
+  }
+  newFiles.add(file);
+}
+
+/**
+ * @param {string } file
+ * @returns {Promise<string | null>}
+ */
+export function readFile(file: string): Promise<string> {
+  if (fs.existsSync(file)) {
+    return fs.readFile(file, 'utf8');
+  }
+  return Promise.resolve('');
 }