diff --git a/.gitignore b/.gitignore
index d25f6310f..8f2c69dea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,12 +21,8 @@ perl/Makefile.config
 /doc/manual/conf-file.json
 /doc/manual/language.json
 /doc/manual/xp-features.json
-/doc/manual/src/command-ref/new-cli
-/doc/manual/src/command-ref/conf-file.md
 /doc/manual/src/command-ref/experimental-features-shortlist.md
 /doc/manual/src/contributing/experimental-feature-descriptions.md
-/doc/manual/src/language/builtins.md
-/doc/manual/src/language/builtin-constants.md
 /doc/manual/src/release-notes/rl-next-generated.md
 
 # /scripts/
diff --git a/doc/manual/book.toml b/doc/manual/book.toml
index e03bed737..ae4ff9b35 100644
--- a/doc/manual/book.toml
+++ b/doc/manual/book.toml
@@ -7,20 +7,22 @@ additional-js = ["redirects.js"]
 edit-url-template = "https://github.com/NixOS/nix/tree/master/doc/manual/{path}"
 git-repository-url = "https://github.com/NixOS/nix"
 
-# Handles replacing @docroot@ with a path to ./src relative to that markdown file.
-[preprocessor.docroot]
-renderers = ["html", "linkcheck"]
-command = "python3 doc/manual/docroot.py"
-# I would have thought that @docroot@ replacement had to be done *before*
-# the link preprocessor gets its hands on this book, but nope it's actually
-# the opposite.
-after = ["links"]
-before = ["anchors"]
+# Handles replacing @docroot@ with a path to ./src relative to that markdown file,
+# {{#include handlebars}}, and the @generated@ syntax used within these. it mostly
+# but not entirely replaces the links preprocessor (which we cannot simply use due
+# to @generated@ files living in a different directory to make meson happy). we do
+# not want to disable the links preprocessor entirely though because that requires
+# disabling *all* built-in preprocessors and selectively reenabling those we want.
+[preprocessor.substitute]
+command = "python3 doc/manual/substitute.py"
+before = ["anchors", "links"]
 
 [preprocessor.anchors]
 renderers = ["html"]
 command = "jq --from-file doc/manual/anchors.jq"
 
+[output.markdown]
+
 [output.linkcheck]
 # no Internet during the build (in the sandbox)
 follow-web-links = false
diff --git a/doc/manual/docroot.py b/doc/manual/docroot.py
deleted file mode 100755
index e95f8abbf..000000000
--- a/doc/manual/docroot.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env python3
-
-from pathlib import Path
-import json
-import os, os.path
-import sys
-
-name = 'process-docroot.py'
-
-def log(*args, **kwargs):
-    kwargs['file'] = sys.stderr
-    return print(f'{name}:', *args, **kwargs)
-
-def replace_docroot(relative_md_path: Path, content: str, book_root: Path):
-    assert not relative_md_path.is_absolute(), f'{relative_md_path=} from mdbook should be relative'
-
-    md_path_abs = book_root / relative_md_path
-    docroot_abs = md_path_abs.parent
-    assert docroot_abs.is_dir(), f'supposed docroot {docroot_abs} is not a directory (cwd={os.getcwd()})'
-
-    # The paths mdbook gives us are relative to the directory with book.toml.
-    # @docroot@ wants to be replaced with the path relative to `src/`.
-    docroot_rel = os.path.relpath(book_root / 'src', start=docroot_abs)
-
-    return content.replace('@docroot@', docroot_rel)
-
-def recursive_replace(data, book_root):
-    match data:
-        case {'sections': sections}:
-            return data | dict(
-                sections = [recursive_replace(section, book_root) for section in sections],
-            )
-        case {'Chapter': chapter}:
-            # Path to the .md file for this chapter, relative to book_root.
-            path_to_chapter = Path('src') / chapter['path']
-            chapter_content = chapter['content']
-
-            return data | dict(
-                Chapter = chapter | dict(
-                    content = replace_docroot(path_to_chapter, chapter_content, book_root),
-                    sub_items = [recursive_replace(sub_item, book_root) for sub_item in chapter['sub_items']],
-                ),
-            )
-
-        case rest:
-            assert False, f'should have been called on a dict, not {type(rest)=}\n\t{rest=}'
-
-def main():
-
-    if len(sys.argv) > 1 and sys.argv[1] == 'supports':
-        log('confirming to mdbook that we support their stuff')
-        return 0
-
-    # mdbook communicates with us over stdin and stdout.
-    # It splorks us a JSON array, the first element describing the context,
-    # the second element describing the book itself,
-    # and then expects us to send it the modified book JSON over stdout.
-
-    context, book = json.load(sys.stdin)
-
-    # book_root is *not* @docroot@. @docroot@ gets replaced with a relative path to `./src/`.
-    # book_root is the directory where book.toml, aka `src`'s parent.
-    book_root = Path(context['root'])
-    assert book_root.exists(), f'{book_root=} does not exist'
-    assert book_root.joinpath('book.toml').is_file(), f'{book_root / "book.toml"} is not a file'
-
-    log('replacing all occurrences of @docroot@ with a relative path')
-
-    # Find @docroot@ in all parts of our recursive book structure.
-    replaced_content = recursive_replace(book, book_root)
-
-    replaced_content_str = json.dumps(replaced_content)
-
-    # Give mdbook our changes.
-    print(replaced_content_str)
-
-    log('done!')
-
-try:
-    sys.exit(main())
-except AssertionError as e:
-    print(f'{name}: INTERNAL ERROR in mdbook preprocessor', file=sys.stderr)
-    print(f'this is a bug in {name}')
-    raise
diff --git a/doc/manual/generate-deps.py b/doc/manual/generate-deps.py
new file mode 100755
index 000000000..297bd3939
--- /dev/null
+++ b/doc/manual/generate-deps.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+
+import glob
+import sys
+
+# meson expects makefile-style dependency declarations, i.e.
+#
+#   target: dependency...
+#
+# meson seems to pass depfiles straight on to ninja even though
+# it also parses the file itself (or at least has code to do so
+# in its tree), so we must live by ninja's rules: only slashes,
+# spaces and octothorpes can be escaped, anything else is taken
+# literally. since the rules for these aren't even the same for
+# all three we will just fail when we encounter any of them (if
+# asserts are off for some reason the depfile will likely point
+# to nonexistant paths, making everything phony and thus fine.)
+for path in glob.glob(sys.argv[1] + '/**', recursive=True):
+    assert '\\' not in path
+    assert ' ' not in path
+    assert '#' not in path
+    print("ignored:", path)
diff --git a/doc/manual/local.mk b/doc/manual/local.mk
index 13ab6da2f..215609f79 100644
--- a/doc/manual/local.mk
+++ b/doc/manual/local.mk
@@ -17,14 +17,14 @@ man-pages := $(foreach n, \
 	nix-hash.1 nix-copy-closure.1 \
 	nix.conf.5 nix-daemon.8 \
 	nix-profiles.5 \
-, $(d)/$(n))
+, doc/manual/generated/in/$(n))
 
 # man pages for subcommands
 # convert from `$(d)/src/command-ref/nix-{1}/{2}.md` to `$(d)/nix-{1}-{2}.1`
 # FIXME: unify with how nix3-cli man pages are generated
 man-pages += $(foreach subcommand, \
 	$(filter-out %opt-common.md %env-common.md, $(wildcard $(d)/src/command-ref/nix-*/*.md)), \
-	$(d)/$(subst /,-,$(subst $(d)/src/command-ref/,,$(subst .md,.1,$(subcommand)))))
+	doc/manual/generated/in/$(subst /,-,$(subst $(d)/src/command-ref/,,$(subst .md,.1,$(subcommand)))))
 
 clean-files += $(d)/*.1 $(d)/*.5 $(d)/*.8
 
@@ -39,77 +39,91 @@ dummy-env = env -i \
 
 nix-eval = $(dummy-env) $(doc_nix) eval --experimental-features nix-command -I nix/corepkgs=corepkgs --store dummy:// --impure --raw
 
-$(d)/nix-env-%.1: $(d)/src/command-ref/nix-env/%.md
+doc/manual/generated/in/nix-env-%.1: doc/manual/generated/out
 	$(trace-gen) doc/manual/render-manpage.sh \
-		--out-no-smarty "$(subst nix-env-,nix-env --,$$(basename "$@" .1))" 1 $^ $^.tmp $@
+		--out-no-smarty "$(subst nix-env-,nix-env --,$$(basename "$@" .1))" 1 \
+		doc/manual/generated/out/markdown/command-ref/nix-env/$*.md \
+		$@
 
-$(d)/nix-store-%.1: $(d)/src/command-ref/nix-store/%.md
+doc/manual/generated/in/nix-store-%.1: doc/manual/generated/out
 	$(trace-gen) doc/manual/render-manpage.sh \
-		--out-no-smarty "$(subst nix-store-,nix-store --,$$(basename "$@" .1))" 1 $^ $^.tmp $@
+		--out-no-smarty "$(subst nix-store-,nix-store --,$$(basename "$@" .1))" 1 \
+		doc/manual/generated/out/markdown/command-ref/nix-store/$*.md \
+		$@
 
 
-$(d)/%.1: $(d)/src/command-ref/%.md
-	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .1)" 1 $^ $^.tmp $@
+doc/manual/generated/in/%.1: doc/manual/generated/out
+	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .1)" 1 \
+		doc/manual/generated/out/markdown/command-ref/$*.md \
+		$@
 
-$(d)/%.8: $(d)/src/command-ref/%.md
-	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .8)" 8 $^ $^.tmp $@
+doc/manual/generated/in/%.8: doc/manual/generated/out
+	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .8)" 8 \
+		doc/manual/generated/out/markdown/command-ref/$*.md \
+		$@
 
-$(d)/nix.conf.5: $(d)/src/command-ref/conf-file.md
-	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .5)" 5 $^ $^.tmp $@
+doc/manual/generated/in/nix.conf.5: doc/manual/generated/out
+	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .5)" 5 \
+		doc/manual/generated/out/markdown/command-ref/conf-file.md \
+		$@
 
-$(d)/nix-profiles.5: $(d)/src/command-ref/files/profiles.md
-	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .5)" 5 $^ $^.tmp $@
+doc/manual/generated/in/nix-profiles.5: doc/manual/generated/out
+	$(trace-gen) doc/manual/render-manpage.sh "$$(basename $@ .5)" 5 \
+		doc/manual/generated/out/markdown/command-ref/files/profiles.md \
+		$@
 
-$(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/utils.nix $(d)/generate-manpage.nix $(doc_nix)
+doc/manual/generated/in/command-ref/new-cli: doc/manual/generated/in/nix.json $(d)/utils.nix $(d)/generate-manpage.nix $(doc_nix)
+	@mkdir -p doc/manual/generated/in/command-ref
 	@rm -rf $@ $@.tmp
 	$(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-manpage.nix true (builtins.readFile $<)'
 	@mv $@.tmp $@
 
-$(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/utils.nix $(d)/src/command-ref/conf-file-prefix.md $(d)/src/command-ref/experimental-features-shortlist.md $(doc_nix)
-	@cat doc/manual/src/command-ref/conf-file-prefix.md > $@.tmp
-	$(trace-gen) $(nix-eval) --expr '(import doc/manual/utils.nix).showSettings { inlineHTML = true; } (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp;
-	@mv $@.tmp $@
+doc/manual/generated/in/command-ref/conf-file.md: doc/manual/generated/in/conf-file.json $(d)/utils.nix doc/manual/generated/in/command-ref/experimental-features-shortlist.md $(doc_nix)
+	@mkdir -p doc/manual/generated/in/command-ref
+	$(trace-gen) $(nix-eval) --expr '(import doc/manual/utils.nix).showSettings { inlineHTML = true; } (builtins.fromJSON (builtins.readFile $<))' >> $@
 
-$(d)/nix.json: $(doc_nix)
+doc/manual/generated/in/nix.json: $(doc_nix)
+	@mkdir -p doc/manual/generated/in
 	$(trace-gen) $(dummy-env) $(doc_nix) __dump-cli > $@.tmp
 	@mv $@.tmp $@
 
-$(d)/conf-file.json: $(doc_nix)
+doc/manual/generated/in/conf-file.json: $(doc_nix)
+	@mkdir -p doc/manual/generated/in
 	$(trace-gen) $(dummy-env) $(doc_nix) show-config --json --experimental-features nix-command > $@.tmp
 	@mv $@.tmp $@
 
-$(d)/src/contributing/experimental-feature-descriptions.md: $(d)/xp-features.json $(d)/utils.nix $(d)/generate-xp-features.nix $(doc_nix)
+doc/manual/generated/in/contributing/experimental-feature-descriptions.md: doc/manual/generated/in/xp-features.json $(d)/utils.nix $(d)/generate-xp-features.nix $(doc_nix)
+	@mkdir -p doc/manual/generated/in/contributing
 	@rm -rf $@ $@.tmp
 	$(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-xp-features.nix (builtins.fromJSON (builtins.readFile $<))'
 	@mv $@.tmp $@
 
-$(d)/src/command-ref/experimental-features-shortlist.md: $(d)/xp-features.json $(d)/utils.nix $(d)/generate-xp-features-shortlist.nix $(doc_nix)
+doc/manual/generated/in/command-ref/experimental-features-shortlist.md: doc/manual/generated/in/xp-features.json $(d)/utils.nix $(d)/generate-xp-features-shortlist.nix $(doc_nix)
+	@mkdir -p doc/manual/generated/in/command-ref
 	@rm -rf $@ $@.tmp
 	$(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-xp-features-shortlist.nix (builtins.fromJSON (builtins.readFile $<))'
 	@mv $@.tmp $@
 
-$(d)/xp-features.json: $(doc_nix)
+doc/manual/generated/in/xp-features.json: $(doc_nix)
 	$(trace-gen) $(dummy-env) NIX_PATH=nix/corepkgs=corepkgs $(doc_nix) __dump-xp-features > $@.tmp
 	@mv $@.tmp $@
 
-$(d)/src/language/builtins.md: $(d)/language.json $(d)/generate-builtins.nix $(d)/src/language/builtins-prefix.md $(doc_nix)
-	@cat doc/manual/src/language/builtins-prefix.md > $@.tmp
-	$(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtins.nix (builtins.fromJSON (builtins.readFile $<)).builtins' >> $@.tmp;
-	@cat doc/manual/src/language/builtins-suffix.md >> $@.tmp
-	@mv $@.tmp $@
+doc/manual/generated/in/language/builtins.md: doc/manual/generated/in/language.json $(d)/generate-builtins.nix $(doc_nix)
+	@mkdir -p doc/manual/generated/in/language
+	$(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtins.nix (builtins.fromJSON (builtins.readFile $<)).builtins' >> $@
 
-$(d)/src/language/builtin-constants.md: $(d)/language.json $(d)/generate-builtin-constants.nix $(d)/src/language/builtin-constants-prefix.md $(doc_nix)
-	@cat doc/manual/src/language/builtin-constants-prefix.md > $@.tmp
-	$(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtin-constants.nix (builtins.fromJSON (builtins.readFile $<)).constants' >> $@.tmp;
-	@cat doc/manual/src/language/builtin-constants-suffix.md >> $@.tmp
-	@mv $@.tmp $@
+doc/manual/generated/in/language/builtin-constants.md: doc/manual/generated/in/language.json $(d)/generate-builtin-constants.nix $(doc_nix)
+	@mkdir -p doc/manual/generated/in/language
+	$(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-builtin-constants.nix (builtins.fromJSON (builtins.readFile $<)).constants' >> $@
 
-$(d)/language.json: $(doc_nix)
+doc/manual/generated/in/language.json: $(doc_nix)
+	@mkdir -p doc/manual/generated/in
 	$(trace-gen) $(dummy-env) NIX_PATH=nix/corepkgs=corepkgs $(doc_nix) __dump-language > $@.tmp
 	@mv $@.tmp $@
 
 # Generate "Upcoming release" notes (or clear it and remove from menu)
-$(d)/src/release-notes/rl-next-generated.md: $(d)/rl-next $(d)/rl-next/*
+doc/manual/generated/in/release-notes/rl-next-generated.md: $(d)/rl-next $(d)/rl-next/*
+	@mkdir -p doc/manual/generated/in/release-notes
 	@if type -p build-release-notes > /dev/null; then \
 		echo "  GEN   " $@; \
 		build-release-notes doc/manual/rl-next > $@; \
@@ -134,9 +148,9 @@ $(mandir)/man1/nix3-manpages: doc/manual/generated/man1/nix3-manpages
 	@mkdir -p $(DESTDIR)$$(dirname $@)
 	$(trace-install) install -m 0644 $$(dirname $<)/* $(DESTDIR)$$(dirname $@)
 
-doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli
+doc/manual/generated/man1/nix3-manpages: doc/manual/generated/out
 	@mkdir -p $(DESTDIR)$$(dirname $@)
-	$(trace-gen) for i in doc/manual/src/command-ref/new-cli/*.md; do \
+	$(trace-gen) for i in doc/manual/generated/out/markdown/command-ref/new-cli/*.md; do \
 		name=$$(basename $$i .md); \
 		tmpFile=$$(mktemp); \
 		if [[ $$name = SUMMARY ]]; then continue; fi; \
@@ -147,11 +161,14 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli
 	done
 	@touch $@
 
-doc/manual/generated/out: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(d)/src/release-notes/rl-next-generated.md $(d)/docroot.py
+doc/manual/generated/out: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md doc/manual/generated/in/command-ref/new-cli doc/manual/generated/in/command-ref/experimental-features-shortlist.md doc/manual/generated/in/contributing/experimental-feature-descriptions.md doc/manual/generated/in/command-ref/conf-file.md doc/manual/generated/in/language/builtins.md doc/manual/generated/in/language/builtin-constants.md doc/manual/generated/in/release-notes/rl-next-generated.md $(d)/substitute.py
 	@rm -rf $@
 	$(trace-gen) \
-		RUST_LOG=warn mdbook build doc/manual -d generated/out 2>&1 \
+		MDBOOK_SUBSTITUTE_SEARCH=doc/manual/generated/in \
+		RUST_LOG=warn \
+		mdbook build doc/manual -d generated/out 2>&1 \
 			| { grep -Fv "because fragment resolution isn't implemented" || :; }
+	@find $@ -iname meson.build -delete
 
 $(docdir)/manual/index.html: doc/manual/generated/out
 	@mkdir -p $(DESTDIR)$(docdir)
diff --git a/doc/manual/meson.build b/doc/manual/meson.build
index 55373d15c..28ff1f587 100644
--- a/doc/manual/meson.build
+++ b/doc/manual/meson.build
@@ -15,7 +15,7 @@ nix_eval_for_docs_common = nix_for_docs + [
 ]
 nix_eval_for_docs = nix_eval_for_docs_common + '--raw'
 
-nix_conf_file_json = custom_target(
+conf_file_json = custom_target(
   command : nix_for_docs + [ 'show-config', '--json' ],
   capture : true,
   output : 'conf-file.json',
@@ -30,7 +30,7 @@ nix_conf_file_md_body = custom_target(
   capture : true,
   input : [
     'utils.nix',
-    nix_conf_file_json,
+    conf_file_json,
   ],
   output : 'conf-file.md.body',
   env : nix_env_for_docs,
@@ -40,7 +40,7 @@ nix_conf_file_md = custom_target(
   command : [ 'cat', '@INPUT@' ],
   capture : true,
   input : [
-    'src/command-ref/conf-file-prefix.md',
+    'src/command-ref/conf-file.md',
     nix_conf_file_md_body,
   ],
   output : 'conf-file.md',
@@ -51,20 +51,83 @@ nix_exp_features_json = custom_target(
   capture : true,
   output : 'xp-features.json',
 )
-nix_exp_feature_shortlist = custom_target(
-  command : nix_eval_for_docs + [
-    '--expr',
-    'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))',
-  ],
-  input : [
-    'generate-xp-features-shortlist.nix',
-    nix_exp_features_json,
-  ],
-  output : 'experimental-features-shortlist.md',
+
+language_json = custom_target(
+  command: [nix, '__dump-language'],
+  output : 'language.json',
   capture : true,
   env : nix_env_for_docs,
 )
 
+nix3_cli_json = custom_target(
+  command : [ nix, '__dump-cli' ],
+  capture : true,
+  output : 'nix.json',
+)
+
+generate_manual_deps = files(
+  'generate-deps.py',
+)
+
+# Generates builtins.md and builtin-constants.md.
+subdir('src/language')
+# Generates new-cli pages, experimental-features-shortlist.md, and conf-file.md.
+subdir('src/command-ref')
+# Generates experimental-feature-descriptions.md.
+subdir('src/contributing')
+# Generates rl-next-generated.md.
+subdir('src/release-notes')
+
+manual = custom_target(
+  'manual',
+  command : [
+    bash,
+    '-euo', 'pipefail',
+    '-c',
+    '''
+        @0@ @INPUT0@ @CURRENT_SOURCE_DIR@ > @DEPFILE@
+        cd @SOURCE_ROOT@
+        @1@ build doc/manual -d @2@ | { grep -Fv "because fragment resolution isn't implemented" || :; }
+        rm -rf @2@/manual
+        mv @2@/html @2@/manual
+        find @2@/manual -iname meson.build -delete
+    '''.format(
+      python.full_path(),
+      mdbook.full_path(),
+      meson.current_build_dir(),
+    ),
+  ],
+  input : [
+    generate_manual_deps,
+    'book.toml',
+    'anchors.jq',
+    'custom.css',
+    nix3_cli_files,
+    experimental_features_shortlist_md,
+    experimental_feature_descriptions_md,
+    conf_file_md,
+    builtins_md,
+    builtin_constants_md,
+    rl_next_generated,
+  ],
+  output : [
+    'manual',
+    'markdown',
+  ],
+  depfile : 'manual.d',
+  env : {
+    'RUST_LOG': 'info',
+    'MDBOOK_SUBSTITUTE_SEARCH': meson.current_build_dir() / 'src',
+  },
+)
+manual_html = manual[0]
+manual_md = manual[1]
+
+install_subdir(
+  manual_html.full_path(),
+  install_dir : datadir / 'doc/nix',
+)
+
 nix_nested_manpages = [
   [ 'nix-env',
     [
@@ -109,17 +172,20 @@ nix_nested_manpages = [
 
 foreach command : nix_nested_manpages
   foreach page : command[1]
+    title = command[0] + ' --' + page
+    section = '1'
     custom_target(
       command : [
         './render-manpage.sh',
         '--out-no-smarty',
-        command[0] + ' --' + page,
-        '1',
-        '@INPUT@',
-        '@OUTPUT@.tmp',
-        '@OUTPUT@',
+        title,
+        section,
+        '@INPUT0@/command-ref' / command[0] / (page + '.md'),
+        '@OUTPUT0@',
+      ],
+      input : [
+        manual_md,
       ],
-      input : 'src/command-ref' / command[0] / (page + '.md'),
       output : command[0] + '-' + page + '.1',
       install : true,
       install_dir : mandir / 'man1',
@@ -127,43 +193,6 @@ foreach command : nix_nested_manpages
   endforeach
 endforeach
 
-nix3_cli_json = custom_target(
-  command : [ nix, '__dump-cli' ],
-  capture : true,
-  output : 'nix.json',
-)
-
-# Intermediate step for manpage generation.
-# This splorks the output of generate-manpage.nix as JSON,
-# which gets written as a directory tree below.
-nix3_cli_files_json = custom_target(
-  command : nix_eval_for_docs_common + [
-    '--json',
-    '--expr',
-    'import @INPUT0@ true (builtins.readFile @INPUT1@)',
-  ],
-  input : [
-    'generate-manpage.nix',
-    nix3_cli_json,
-  ],
-  capture : true,
-  output : 'new-cli.json',
-  env : nix_env_for_docs,
-)
-nix3_cli_files = custom_target(
-  command : [
-    python,
-    '@INPUT0@',
-    '-i', '@INPUT1@',
-    '-o', '@OUTPUT@',
-  ],
-  input : [
-    'json-to-tree.py',
-    nix3_cli_files_json,
-  ],
-  output : 'new-cli',
-)
-
 nix3_manpages = [
   'nix3-build',
   'nix3-bundle',
@@ -254,16 +283,20 @@ nix3_manpages = [
 ]
 
 foreach page : nix3_manpages
+  section = '1'
   custom_target(
     command : [
-      './render-manpage.sh',
+      bash,
+      '@INPUT0@',
       page,
-      '1',
-      '@INPUT0@/' + page + '.md',
-      '@OUTPUT@.tmp',
+      section,
+      '@INPUT1@/command-ref/new-cli/@0@.md'.format(page),
       '@OUTPUT@',
     ],
-    input : nix3_cli_files,
+    input : [
+      'render-manpage.sh',
+      manual_md,
+    ],
     output : page + '.1',
     install : true,
     install_dir : mandir / 'man1',
@@ -281,24 +314,30 @@ nix_manpages = [
   [ 'nix-channel', 1 ],
   [ 'nix-hash', 1 ],
   [ 'nix-copy-closure', 1 ],
-  [ 'nix.conf', 5, nix_conf_file_md, nix_exp_feature_shortlist ],
+  [ 'nix.conf', 5, 'conf-file.md' ],
   [ 'nix-daemon', 8 ],
-  [ 'nix-profiles', 5, 'src/command-ref/files/profiles.md', nix_exp_feature_shortlist ],
+  [ 'nix-profiles', 5, 'files/profiles.md' ],
 ]
 
 foreach entry : nix_manpages
+  title = entry[0]
+  # nix.conf.5 and nix-profiles.5 are based off of conf-file.md and files/profiles.md,
+  # rather than a stem identical to its mdbook source.
+  # Therefore we use an optional third element of this array to override the name pattern
+  md_file = entry.get(2, title + '.md')
+  section = entry[1].to_string()
   custom_target(
     command : [
-      './render-manpage.sh',
-      entry[0],
-      entry[1].to_string(),
+      bash,
       '@INPUT0@',
-      '@OUTPUT@.tmp',
+      title,
+      section,
+      '@INPUT1@/command-ref/@0@'.format(md_file),
       '@OUTPUT@',
-      meson.current_build_dir(),
     ],
     input : [
-      entry.get(2, 'src/command-ref' / (entry[0] + '.md')),
+      'render-manpage.sh',
+      manual_md,
       entry.get(3, []),
     ],
     output : '@0@.@1@'.format(entry[0], entry[1]),
diff --git a/doc/manual/render-manpage.sh b/doc/manual/render-manpage.sh
index 42f13911e..7d83253cb 100755
--- a/doc/manual/render-manpage.sh
+++ b/doc/manual/render-manpage.sh
@@ -9,14 +9,17 @@ if [ "$1" = --out-no-smarty ]; then
     shift
 fi
 
+[ "$#" = 4 ] || {
+    echo "wrong number of args passed" >&2
+    exit 1
+}
+
 title="$1"
 section="$2"
 infile="$3"
-tmpfile="$4"
-outfile="$5"
+outfile="$4"
 
-printf "Title: %s\n\n" "$title" > "$tmpfile"
-cat "$infile" >> "$tmpfile"
-"$(dirname "$0")"/process-includes.sh "$infile" "$tmpfile"
-lowdown -sT man --nroff-nolinks $lowdown_args -M section="$section" "$tmpfile" -o "$outfile"
-rm "$tmpfile"
+(
+    printf "Title: %s\n\n" "$title"
+    cat "$infile"
+) | lowdown -sT man --nroff-nolinks $lowdown_args -M section="$section" -o "$outfile"
diff --git a/doc/manual/src/command-ref/conf-file-prefix.md b/doc/manual/src/command-ref/conf-file.md
similarity index 98%
rename from doc/manual/src/command-ref/conf-file-prefix.md
rename to doc/manual/src/command-ref/conf-file.md
index 822777eff..980cf8c14 100644
--- a/doc/manual/src/command-ref/conf-file-prefix.md
+++ b/doc/manual/src/command-ref/conf-file.md
@@ -67,3 +67,5 @@ Configuration options can be set on the command line, overriding the values set
 The `extra-` prefix is supported for settings that take a list of items (e.g. `--extra-trusted users alice` or `--option extra-trusted-users alice`).
 
 # Available settings
+
+{{#include @generated@/command-ref/conf-file.md}}
diff --git a/doc/manual/src/command-ref/meson.build b/doc/manual/src/command-ref/meson.build
new file mode 100644
index 000000000..6405ce685
--- /dev/null
+++ b/doc/manual/src/command-ref/meson.build
@@ -0,0 +1,65 @@
+xp_features_json = custom_target(
+  command : [nix, '__dump-xp-features'],
+  capture : true,
+  output : 'xp-features.json',
+)
+
+experimental_features_shortlist_md = custom_target(
+  command : nix_eval_for_docs + [
+    '--expr',
+    'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))',
+  ],
+  input : [
+    '../../generate-xp-features-shortlist.nix',
+    xp_features_json,
+  ],
+  capture : true,
+  output : 'experimental-features-shortlist.md',
+  env : nix_env_for_docs,
+)
+
+# Intermediate step for manpage generation.
+# This splorks the output of generate-manpage.nix as JSON,
+# which gets written as a directory tree below.
+nix3_cli_files_json = custom_target(
+  command : nix_eval_for_docs_common + [
+    '--json',
+    '--expr',
+    'import @INPUT0@ true (builtins.readFile @INPUT1@)',
+  ],
+  input : [
+    '../../generate-manpage.nix',
+    nix3_cli_json,
+  ],
+  capture : true,
+  output : 'new-cli.json',
+  env : nix_env_for_docs,
+)
+nix3_cli_files = custom_target(
+  command : [
+    python,
+    '@INPUT0@',
+    '-i', '@INPUT1@',
+    '-o', '@OUTPUT@',
+  ],
+  input : [
+    '../../json-to-tree.py',
+    nix3_cli_files_json,
+  ],
+  output : 'new-cli',
+)
+
+conf_file_md = custom_target(
+  command : [
+    nix_eval_for_docs,
+    '--expr',
+    '(import @INPUT0@).showSettings { inlineHTML = true; } (builtins.fromJSON (builtins.readFile @INPUT1@))',
+  ],
+  capture : true,
+  input : [
+    '../../utils.nix',
+    conf_file_json,
+    experimental_features_shortlist_md,
+  ],
+  output : 'conf-file.md',
+)
diff --git a/doc/manual/src/command-ref/new-cli/nix.md b/doc/manual/src/command-ref/new-cli/nix.md
new file mode 100644
index 000000000..586800c40
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-build.md b/doc/manual/src/command-ref/new-cli/nix3-build.md
new file mode 100644
index 000000000..08d067501
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-build.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-build.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-bundle.md b/doc/manual/src/command-ref/new-cli/nix3-bundle.md
new file mode 100644
index 000000000..ebfcca2f4
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-bundle.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-bundle.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-copy.md b/doc/manual/src/command-ref/new-cli/nix3-copy.md
new file mode 100644
index 000000000..29fefff59
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-copy.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-copy.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-daemon.md b/doc/manual/src/command-ref/new-cli/nix3-daemon.md
new file mode 100644
index 000000000..a0c0fd02e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-daemon.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-daemon.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-derivation-add.md b/doc/manual/src/command-ref/new-cli/nix3-derivation-add.md
new file mode 100644
index 000000000..4ec3ff16c
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-derivation-add.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-derivation-add.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-derivation-show.md b/doc/manual/src/command-ref/new-cli/nix3-derivation-show.md
new file mode 100644
index 000000000..8cf5dbe0a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-derivation-show.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-derivation-show.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-derivation.md b/doc/manual/src/command-ref/new-cli/nix3-derivation.md
new file mode 100644
index 000000000..3a80ebbb5
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-derivation.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-derivation.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-develop.md b/doc/manual/src/command-ref/new-cli/nix3-develop.md
new file mode 100644
index 000000000..55cb03987
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-develop.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-develop.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-doctor.md b/doc/manual/src/command-ref/new-cli/nix3-doctor.md
new file mode 100644
index 000000000..f6e0b401a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-doctor.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-doctor.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-edit.md b/doc/manual/src/command-ref/new-cli/nix3-edit.md
new file mode 100644
index 000000000..a841f2456
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-edit.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-edit.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-eval.md b/doc/manual/src/command-ref/new-cli/nix3-eval.md
new file mode 100644
index 000000000..16ac2517e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-eval.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-eval.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-archive.md b/doc/manual/src/command-ref/new-cli/nix3-flake-archive.md
new file mode 100644
index 000000000..5e4cdb1b2
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-archive.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-archive.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-check.md b/doc/manual/src/command-ref/new-cli/nix3-flake-check.md
new file mode 100644
index 000000000..677c964aa
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-check.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-check.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-clone.md b/doc/manual/src/command-ref/new-cli/nix3-flake-clone.md
new file mode 100644
index 000000000..be53e26ce
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-clone.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-clone.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-info.md b/doc/manual/src/command-ref/new-cli/nix3-flake-info.md
new file mode 100644
index 000000000..118e09b23
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-info.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-info.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-init.md b/doc/manual/src/command-ref/new-cli/nix3-flake-init.md
new file mode 100644
index 000000000..48cc0f3ef
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-init.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-init.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-lock.md b/doc/manual/src/command-ref/new-cli/nix3-flake-lock.md
new file mode 100644
index 000000000..9d6845653
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-lock.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-lock.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-metadata.md b/doc/manual/src/command-ref/new-cli/nix3-flake-metadata.md
new file mode 100644
index 000000000..8930bb5e1
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-metadata.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-metadata.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-new.md b/doc/manual/src/command-ref/new-cli/nix3-flake-new.md
new file mode 100644
index 000000000..5b7d9ebdf
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-new.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-new.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-prefetch.md b/doc/manual/src/command-ref/new-cli/nix3-flake-prefetch.md
new file mode 100644
index 000000000..69b41faae
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-prefetch.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-prefetch.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-show.md b/doc/manual/src/command-ref/new-cli/nix3-flake-show.md
new file mode 100644
index 000000000..59755e960
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-show.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-show.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake-update.md b/doc/manual/src/command-ref/new-cli/nix3-flake-update.md
new file mode 100644
index 000000000..f897e504f
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake-update.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake-update.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-flake.md b/doc/manual/src/command-ref/new-cli/nix3-flake.md
new file mode 100644
index 000000000..97d38f1bb
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-flake.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-flake.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-fmt.md b/doc/manual/src/command-ref/new-cli/nix3-fmt.md
new file mode 100644
index 000000000..ed31de3f7
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-fmt.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-fmt.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-file.md b/doc/manual/src/command-ref/new-cli/nix3-hash-file.md
new file mode 100644
index 000000000..b81754594
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-file.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-file.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-path.md b/doc/manual/src/command-ref/new-cli/nix3-hash-path.md
new file mode 100644
index 000000000..9928928f2
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-path.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-path.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-to-base16.md b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base16.md
new file mode 100644
index 000000000..fb04ccee8
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base16.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-to-base16.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-to-base32.md b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base32.md
new file mode 100644
index 000000000..2e40c1077
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base32.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-to-base32.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-to-base64.md b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base64.md
new file mode 100644
index 000000000..8ec8ecbc6
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-to-base64.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-to-base64.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash-to-sri.md b/doc/manual/src/command-ref/new-cli/nix3-hash-to-sri.md
new file mode 100644
index 000000000..00298dff0
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash-to-sri.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash-to-sri.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-hash.md b/doc/manual/src/command-ref/new-cli/nix3-hash.md
new file mode 100644
index 000000000..68ab9862e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-hash.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-hash.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-help-stores.md b/doc/manual/src/command-ref/new-cli/nix3-help-stores.md
new file mode 100644
index 000000000..f397cc046
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-help-stores.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-help-stores.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-help.md b/doc/manual/src/command-ref/new-cli/nix3-help.md
new file mode 100644
index 000000000..306f1ea7a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-help.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-help.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-key-convert-secret-to-public.md b/doc/manual/src/command-ref/new-cli/nix3-key-convert-secret-to-public.md
new file mode 100644
index 000000000..9f6e1be3a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-key-convert-secret-to-public.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-key-convert-secret-to-public.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-key-generate-secret.md b/doc/manual/src/command-ref/new-cli/nix3-key-generate-secret.md
new file mode 100644
index 000000000..28225d3c8
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-key-generate-secret.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-key-generate-secret.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-key.md b/doc/manual/src/command-ref/new-cli/nix3-key.md
new file mode 100644
index 000000000..7df72030b
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-key.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-key.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-log.md b/doc/manual/src/command-ref/new-cli/nix3-log.md
new file mode 100644
index 000000000..8e590bfde
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-log.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-log.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-nar-cat.md b/doc/manual/src/command-ref/new-cli/nix3-nar-cat.md
new file mode 100644
index 000000000..21e137cc5
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-nar-cat.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-nar-cat.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-nar-dump-path.md b/doc/manual/src/command-ref/new-cli/nix3-nar-dump-path.md
new file mode 100644
index 000000000..39d4a0f8d
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-nar-dump-path.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-nar-dump-path.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-nar-ls.md b/doc/manual/src/command-ref/new-cli/nix3-nar-ls.md
new file mode 100644
index 000000000..60c75831a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-nar-ls.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-nar-ls.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-nar.md b/doc/manual/src/command-ref/new-cli/nix3-nar.md
new file mode 100644
index 000000000..91217904d
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-nar.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-nar.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-path-info.md b/doc/manual/src/command-ref/new-cli/nix3-path-info.md
new file mode 100644
index 000000000..8db34e0f1
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-path-info.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-path-info.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-print-dev-env.md b/doc/manual/src/command-ref/new-cli/nix3-print-dev-env.md
new file mode 100644
index 000000000..7754780eb
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-print-dev-env.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-print-dev-env.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-diff-closures.md b/doc/manual/src/command-ref/new-cli/nix3-profile-diff-closures.md
new file mode 100644
index 000000000..eba30aa16
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-diff-closures.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-diff-closures.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-history.md b/doc/manual/src/command-ref/new-cli/nix3-profile-history.md
new file mode 100644
index 000000000..85e8afc44
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-history.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-history.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-install.md b/doc/manual/src/command-ref/new-cli/nix3-profile-install.md
new file mode 100644
index 000000000..bb530cd9a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-install.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-install.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-list.md b/doc/manual/src/command-ref/new-cli/nix3-profile-list.md
new file mode 100644
index 000000000..7f6e7a3fa
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-list.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-list.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-remove.md b/doc/manual/src/command-ref/new-cli/nix3-profile-remove.md
new file mode 100644
index 000000000..3651cb086
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-remove.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-remove.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-rollback.md b/doc/manual/src/command-ref/new-cli/nix3-profile-rollback.md
new file mode 100644
index 000000000..9c73cf2f0
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-rollback.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-rollback.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-upgrade.md b/doc/manual/src/command-ref/new-cli/nix3-profile-upgrade.md
new file mode 100644
index 000000000..1b498cfd0
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-upgrade.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-upgrade.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile-wipe-history.md b/doc/manual/src/command-ref/new-cli/nix3-profile-wipe-history.md
new file mode 100644
index 000000000..662169dbe
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile-wipe-history.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile-wipe-history.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-profile.md b/doc/manual/src/command-ref/new-cli/nix3-profile.md
new file mode 100644
index 000000000..c98b611a3
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-profile.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-profile.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-realisation-info.md b/doc/manual/src/command-ref/new-cli/nix3-realisation-info.md
new file mode 100644
index 000000000..20d172987
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-realisation-info.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-realisation-info.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-realisation.md b/doc/manual/src/command-ref/new-cli/nix3-realisation.md
new file mode 100644
index 000000000..7c176f1c6
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-realisation.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-realisation.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-registry-add.md b/doc/manual/src/command-ref/new-cli/nix3-registry-add.md
new file mode 100644
index 000000000..9bb491fb3
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-registry-add.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-registry-add.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-registry-list.md b/doc/manual/src/command-ref/new-cli/nix3-registry-list.md
new file mode 100644
index 000000000..42c595eaf
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-registry-list.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-registry-list.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-registry-pin.md b/doc/manual/src/command-ref/new-cli/nix3-registry-pin.md
new file mode 100644
index 000000000..256f95cab
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-registry-pin.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-registry-pin.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-registry-remove.md b/doc/manual/src/command-ref/new-cli/nix3-registry-remove.md
new file mode 100644
index 000000000..4e2dda4f7
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-registry-remove.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-registry-remove.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-registry.md b/doc/manual/src/command-ref/new-cli/nix3-registry.md
new file mode 100644
index 000000000..5551d19b3
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-registry.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-registry.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-repl.md b/doc/manual/src/command-ref/new-cli/nix3-repl.md
new file mode 100644
index 000000000..da82694fd
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-repl.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-repl.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-run.md b/doc/manual/src/command-ref/new-cli/nix3-run.md
new file mode 100644
index 000000000..1114efd9c
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-run.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-run.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-search.md b/doc/manual/src/command-ref/new-cli/nix3-search.md
new file mode 100644
index 000000000..b72a5add5
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-search.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-search.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-shell.md b/doc/manual/src/command-ref/new-cli/nix3-shell.md
new file mode 100644
index 000000000..82f0af1e2
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-shell.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-shell.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-show-config.md b/doc/manual/src/command-ref/new-cli/nix3-show-config.md
new file mode 100644
index 000000000..060fc065d
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-show-config.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-show-config.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-add-file.md b/doc/manual/src/command-ref/new-cli/nix3-store-add-file.md
new file mode 100644
index 000000000..fa30cf660
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-add-file.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-add-file.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-add-path.md b/doc/manual/src/command-ref/new-cli/nix3-store-add-path.md
new file mode 100644
index 000000000..88ccd4b51
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-add-path.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-add-path.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-cat.md b/doc/manual/src/command-ref/new-cli/nix3-store-cat.md
new file mode 100644
index 000000000..de617fc4e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-cat.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-cat.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-copy-log.md b/doc/manual/src/command-ref/new-cli/nix3-store-copy-log.md
new file mode 100644
index 000000000..44998311f
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-copy-log.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-copy-log.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-copy-sigs.md b/doc/manual/src/command-ref/new-cli/nix3-store-copy-sigs.md
new file mode 100644
index 000000000..cc03ab44e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-copy-sigs.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-copy-sigs.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-delete.md b/doc/manual/src/command-ref/new-cli/nix3-store-delete.md
new file mode 100644
index 000000000..42a05162d
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-delete.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-delete.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-diff-closures.md b/doc/manual/src/command-ref/new-cli/nix3-store-diff-closures.md
new file mode 100644
index 000000000..fe6bcd235
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-diff-closures.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-diff-closures.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-dump-path.md b/doc/manual/src/command-ref/new-cli/nix3-store-dump-path.md
new file mode 100644
index 000000000..c4f34f450
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-dump-path.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-dump-path.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-gc.md b/doc/manual/src/command-ref/new-cli/nix3-store-gc.md
new file mode 100644
index 000000000..da79527ab
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-gc.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-gc.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-ls.md b/doc/manual/src/command-ref/new-cli/nix3-store-ls.md
new file mode 100644
index 000000000..fb64bcb21
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-ls.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-ls.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-make-content-addressed.md b/doc/manual/src/command-ref/new-cli/nix3-store-make-content-addressed.md
new file mode 100644
index 000000000..0a4b6c93a
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-make-content-addressed.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-make-content-addressed.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-optimise.md b/doc/manual/src/command-ref/new-cli/nix3-store-optimise.md
new file mode 100644
index 000000000..cc84844d7
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-optimise.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-optimise.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-path-from-hash-part.md b/doc/manual/src/command-ref/new-cli/nix3-store-path-from-hash-part.md
new file mode 100644
index 000000000..c9f4d4f1c
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-path-from-hash-part.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-path-from-hash-part.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-ping.md b/doc/manual/src/command-ref/new-cli/nix3-store-ping.md
new file mode 100644
index 000000000..e199e904e
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-ping.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-ping.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-prefetch-file.md b/doc/manual/src/command-ref/new-cli/nix3-store-prefetch-file.md
new file mode 100644
index 000000000..c72742413
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-prefetch-file.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-prefetch-file.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-repair.md b/doc/manual/src/command-ref/new-cli/nix3-store-repair.md
new file mode 100644
index 000000000..7ad9058d8
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-repair.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-repair.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-sign.md b/doc/manual/src/command-ref/new-cli/nix3-store-sign.md
new file mode 100644
index 000000000..ec28f973b
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-sign.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-sign.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store-verify.md b/doc/manual/src/command-ref/new-cli/nix3-store-verify.md
new file mode 100644
index 000000000..1d115da04
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store-verify.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store-verify.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-store.md b/doc/manual/src/command-ref/new-cli/nix3-store.md
new file mode 100644
index 000000000..1a4c3b350
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-store.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-store.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-upgrade-nix.md b/doc/manual/src/command-ref/new-cli/nix3-upgrade-nix.md
new file mode 100644
index 000000000..39f095573
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-upgrade-nix.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-upgrade-nix.md}}
diff --git a/doc/manual/src/command-ref/new-cli/nix3-why-depends.md b/doc/manual/src/command-ref/new-cli/nix3-why-depends.md
new file mode 100644
index 000000000..c6629ffcd
--- /dev/null
+++ b/doc/manual/src/command-ref/new-cli/nix3-why-depends.md
@@ -0,0 +1 @@
+{{#include @generated@/command-ref/new-cli/nix3-why-depends.md}}
diff --git a/doc/manual/src/contributing/experimental-features.md b/doc/manual/src/contributing/experimental-features.md
index ad5cffa91..b2dddeb8e 100644
--- a/doc/manual/src/contributing/experimental-features.md
+++ b/doc/manual/src/contributing/experimental-features.md
@@ -92,4 +92,4 @@ This means that experimental features and RFCs are orthogonal mechanisms, and ca
 
 # Currently available experimental features
 
-{{#include ./experimental-feature-descriptions.md}}
+{{#include @generated@/contributing/experimental-feature-descriptions.md}}
diff --git a/doc/manual/src/contributing/meson.build b/doc/manual/src/contributing/meson.build
new file mode 100644
index 000000000..2929578c8
--- /dev/null
+++ b/doc/manual/src/contributing/meson.build
@@ -0,0 +1,15 @@
+# Intermediate step for experimental-feature-descriptions.md.
+# This splorks the output of generate-xp-features.nix as JSON,
+# which gets written as a directory tree below.
+experimental_feature_descriptions_md = custom_target(
+  command : nix_eval_for_docs + [
+    '--expr',
+    'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))',
+  ],
+  input : [
+    '../../generate-xp-features.nix',
+    xp_features_json,
+  ],
+  capture : true,
+  output : 'experimental-feature-descriptions.md',
+)
diff --git a/doc/manual/src/language/builtin-constants-suffix.md b/doc/manual/src/language/builtin-constants-suffix.md
deleted file mode 100644
index a74db2857..000000000
--- a/doc/manual/src/language/builtin-constants-suffix.md
+++ /dev/null
@@ -1 +0,0 @@
-</dl>
diff --git a/doc/manual/src/language/builtin-constants-prefix.md b/doc/manual/src/language/builtin-constants.md
similarity index 58%
rename from doc/manual/src/language/builtin-constants-prefix.md
rename to doc/manual/src/language/builtin-constants.md
index 50f43006d..74e87146f 100644
--- a/doc/manual/src/language/builtin-constants-prefix.md
+++ b/doc/manual/src/language/builtin-constants.md
@@ -3,3 +3,7 @@
 These constants are built into the Nix language evaluator:
 
 <dl>
+
+{{#include @generated@/language/builtin-constants.md}}
+
+</dl>
diff --git a/doc/manual/src/language/builtins-suffix.md b/doc/manual/src/language/builtins-suffix.md
deleted file mode 100644
index a74db2857..000000000
--- a/doc/manual/src/language/builtins-suffix.md
+++ /dev/null
@@ -1 +0,0 @@
-</dl>
diff --git a/doc/manual/src/language/builtins-prefix.md b/doc/manual/src/language/builtins.md
similarity index 92%
rename from doc/manual/src/language/builtins-prefix.md
rename to doc/manual/src/language/builtins.md
index 7b2321466..2a3972f05 100644
--- a/doc/manual/src/language/builtins-prefix.md
+++ b/doc/manual/src/language/builtins.md
@@ -14,3 +14,7 @@ For convenience, some built-ins can be accessed directly:
   <dt id="builtins-derivation"><a href="#builtins-derivation"><code>derivation <var>attrs</var></code></a></dt>
   <dd><p><var>derivation</var> is described in
          <a href="derivations.md">its own section</a>.</p></dd>
+
+{{#include @generated@/language/builtins.md}}
+
+</dl>
diff --git a/doc/manual/src/language/meson.build b/doc/manual/src/language/meson.build
new file mode 100644
index 000000000..bc0d147f3
--- /dev/null
+++ b/doc/manual/src/language/meson.build
@@ -0,0 +1,27 @@
+builtins_md = custom_target(
+  command : nix_eval_for_docs + [
+    '--expr',
+    'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@)).builtins',
+  ],
+  capture : true,
+  input : [
+    '../../generate-builtins.nix',
+    language_json,
+  ],
+  output : 'builtins.md',
+  env : nix_env_for_docs,
+)
+
+builtin_constants_md = custom_target(
+  command : nix_eval_for_docs + [
+    '--expr',
+    'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@)).constants',
+  ],
+  capture : true,
+  input : [
+    '../../generate-builtin-constants.nix',
+    language_json,
+  ],
+  output : 'builtin-constants.md',
+  env : nix_env_for_docs,
+)
diff --git a/doc/manual/src/release-notes/meson.build b/doc/manual/src/release-notes/meson.build
new file mode 100644
index 000000000..87e74c248
--- /dev/null
+++ b/doc/manual/src/release-notes/meson.build
@@ -0,0 +1,20 @@
+rl_next_generated = custom_target(
+  command : [
+    'bash',
+    '-c',
+    '''
+      if type -p build-release-notes > /dev/null; then
+        build-release-notes @CURRENT_SOURCE_DIR@
+      fi
+      @0@ @INPUT0@ @CURRENT_SOURCE_DIR@ > @DEPFILE@
+    '''.format(
+      python.full_path(),
+    ),
+  ],
+  input : [
+    generate_manual_deps,
+  ],
+  output : 'rl-next-generated.md',
+  capture : true,
+  depfile : 'rl-next.d',
+)
diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
index 35033f7b6..bf4bb194b 100644
--- a/doc/manual/src/release-notes/rl-next.md
+++ b/doc/manual/src/release-notes/rl-next.md
@@ -1,3 +1,3 @@
 # Upcoming release
 
-{{#include rl-next-generated.md}}
+{{#include @generated@/release-notes/rl-next-generated.md}}
diff --git a/doc/manual/substitute.py b/doc/manual/substitute.py
new file mode 100755
index 000000000..18b7bed68
--- /dev/null
+++ b/doc/manual/substitute.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+import json
+import os, os.path
+import sys
+
+name = 'substitute.py'
+
+def log(*args, **kwargs):
+    kwargs['file'] = sys.stderr
+    return print(f'{name}:', *args, **kwargs)
+
+def do_include(content: str, relative_md_path: Path, source_root: Path, search_path: Path):
+    assert not relative_md_path.is_absolute(), f'{relative_md_path=} from mdbook should be relative'
+
+    md_path_abs = source_root / relative_md_path
+    var_abs = md_path_abs.parent
+    assert var_abs.is_dir(), f'supposed directory {var_abs} is not a directory (cwd={os.getcwd()})'
+
+    lines = []
+    for l in content.splitlines(keepends=True):
+        if l.strip().startswith("{{#include "):
+            requested = l.strip()[11:][:-2]
+            if requested.startswith("@generated@/"):
+                included = search_path / Path(requested[12:])
+                requested = included.relative_to(search_path)
+            else:
+                included = source_root / relative_md_path.parent / requested
+                requested = included.resolve().relative_to(source_root)
+            assert included.exists(), f"{requested} not found at {included}"
+            lines.append(do_include(included.read_text(), requested, source_root, search_path) + "\n")
+        else:
+            lines.append(l)
+    return "".join(lines)
+
+def recursive_replace(data, book_root, search_path):
+    match data:
+        case {'sections': sections}:
+            return data | dict(
+                sections = [recursive_replace(section, book_root, search_path) for section in sections],
+            )
+        case {'Chapter': chapter}:
+            path_to_chapter = Path(chapter['path'])
+            chapter_content = chapter['content']
+
+            return data | dict(
+                Chapter = chapter | dict(
+                    # first process includes. this must happen before docroot processing since
+                    # mdbook does not see these included files, only the final agglomeration.
+                    content = do_include(
+                        chapter_content,
+                        path_to_chapter,
+                        book_root,
+                        search_path
+                    ).replace(
+                        '@docroot@',
+                        ("../" * len(path_to_chapter.parent.parts) or "./")[:-1]
+                    ),
+                    sub_items = [
+                        recursive_replace(sub_item, book_root, search_path)
+                        for sub_item in chapter['sub_items']
+                    ],
+                ),
+            )
+
+        case rest:
+            assert False, f'should have been called on a dict, not {type(rest)=}\n\t{rest=}'
+
+def main():
+
+    if len(sys.argv) > 1 and sys.argv[1] == 'supports':
+        return 0
+
+    # mdbook communicates with us over stdin and stdout.
+    # It splorks us a JSON array, the first element describing the context,
+    # the second element describing the book itself,
+    # and then expects us to send it the modified book JSON over stdout.
+
+    context, book = json.load(sys.stdin)
+
+    # book_root is the directory where book contents leave (ie, src/)
+    book_root = Path(context['root']) / context['config']['book']['src']
+
+    # includes pointing into @generated@ will look here
+    search_path = Path(os.environ['MDBOOK_SUBSTITUTE_SEARCH'])
+
+    # Find @var@ in all parts of our recursive book structure.
+    replaced_content = recursive_replace(book, book_root, search_path)
+
+    replaced_content_str = json.dumps(replaced_content)
+
+    # Give mdbook our changes.
+    print(replaced_content_str)
+
+try:
+    sys.exit(main())
+except AssertionError as e:
+    print(f'{name}: INTERNAL ERROR in mdbook preprocessor', file=sys.stderr)
+    print(f'this is a bug in {name}', file=sys.stderr)
+    raise
diff --git a/meson.build b/meson.build
index 22739a4ab..bc55c64c4 100644
--- a/meson.build
+++ b/meson.build
@@ -258,6 +258,10 @@ dot = find_program('dot', required : false)
 pymod = import('python')
 python = pymod.find_installation('python3')
 
+if enable_docs
+  mdbook = find_program('mdbook')
+endif
+
 # Used to workaround https://github.com/mesonbuild/meson/issues/2320 in src/nix/meson.build.
 installcmd = find_program('install')
 
diff --git a/src/libutil/config.hh b/src/libutil/config.hh
index 01e1239b3..fb433c607 100644
--- a/src/libutil/config.hh
+++ b/src/libutil/config.hh
@@ -436,7 +436,7 @@ struct ExperimentalFeatureSettings : Config {
 
           The following experimental features are available:
 
-          {{#include experimental-features-shortlist.md}}
+          {{#include @generated@/command-ref/experimental-features-shortlist.md}}
 
           Experimental features are [further documented in the manual](@docroot@/contributing/experimental-features.md).
         )"};