diff --git a/Documentation/Doxyfile-internal.in b/Documentation/Doxyfile-internal.in
index 5343bc2b131c..a422bb0719da 100644
--- a/Documentation/Doxyfile-internal.in
+++ b/Documentation/Doxyfile-internal.in
@@ -3,6 +3,8 @@
 @INCLUDE_PATH          = @TOP_BUILDDIR@/Documentation
 @INCLUDE               = Doxyfile-common
 
+GENERATE_TAGFILE       = @TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml
+
 HIDE_UNDOC_CLASSES     = NO
 HIDE_UNDOC_MEMBERS     = NO
 HTML_OUTPUT            = internal-api-html
diff --git a/Documentation/Doxyfile-public.in b/Documentation/Doxyfile-public.in
index 36bb57584a07..c3a8b0dd003a 100644
--- a/Documentation/Doxyfile-public.in
+++ b/Documentation/Doxyfile-public.in
@@ -3,6 +3,8 @@
 @INCLUDE_PATH          = @TOP_BUILDDIR@/Documentation
 @INCLUDE               = Doxyfile-common
 
+GENERATE_TAGFILE       = @TOP_BUILDDIR@/Documentation/api-html/tagfile.xml
+
 HIDE_UNDOC_CLASSES     = YES
 HIDE_UNDOC_MEMBERS     = YES
 HTML_OUTPUT            = api-html
diff --git a/Documentation/conf.py b/Documentation/conf.py
index 870937289eb4..f50be60a1559 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -37,7 +37,8 @@ author = 'The libcamera documentation authors'
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.graphviz'
+    'sphinx.ext.graphviz',
+    'sphinxcontrib.doxylink',
 ]
 
 graphviz_output_format = 'svg'
@@ -71,6 +72,16 @@ exclude_patterns = [
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = None
 
+doxylink = {
+    'doxy-pub': (
+        'Documentation/api-html/tagfile.xml',
+        '../api-html/',
+    ),
+    'doxy-int': (
+        'Documentation/internal-api-html/tagfile.xml',
+        '../internal-api-html/',
+    ),
+}
 
 # -- Options for HTML output -------------------------------------------------
 
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 3afdcc1a87af..87918bf9a921 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -81,16 +81,16 @@ if doxygen.found() and dot.found()
                                  '@INPUT@',
                              ])
 
-    custom_target('doxygen-public',
-                  input : [
-                      doxyfile,
-                      doxyfile_common,
-                  ],
-                  output : 'api-html',
-                  command : [doxygen, doxyfile],
-                  install : true,
-                  install_dir : doc_install_dir,
-                  install_tag : 'doc')
+    doxygen_public = custom_target('doxygen-public',
+                                   input : [
+                                       doxyfile,
+                                       doxyfile_common,
+                                   ],
+                                   output : 'api-html',
+                                   command : [doxygen, doxyfile],
+                                   install : true,
+                                   install_dir : doc_install_dir,
+                                   install_tag : 'doc')
 
     # This is the internal documentation, which hard-codes a list of directories
     # to parse in its doxyfile.
@@ -99,18 +99,18 @@ if doxygen.found() and dot.found()
                               output : 'Doxyfile-internal',
                               configuration : cdata)
 
-    custom_target('doxygen-internal',
-                  input : [
-                      doxyfile,
-                      doxyfile_common,
-                      doxygen_public_input,
-                      doxygen_internal_input,
-                  ],
-                  output : 'internal-api-html',
-                  command : [doxygen, doxyfile],
-                  install : true,
-                  install_dir : doc_install_dir,
-                  install_tag : 'doc-internal')
+    doxygen_internal = custom_target('doxygen-internal',
+                                     input : [
+                                         doxyfile,
+                                         doxyfile_common,
+                                         doxygen_public_input,
+                                         doxygen_internal_input,
+                                     ],
+                                     output : 'internal-api-html',
+                                     command : [doxygen, doxyfile],
+                                     install : true,
+                                     install_dir : doc_install_dir,
+                                     install_tag : 'doc-internal')
 endif
 
 #
@@ -154,6 +154,10 @@ if sphinx.found()
                   input : docs_sources,
                   output : 'html',
                   build_by_default : true,
+                  depends : [
+                      doxygen_public,
+                      doxygen_internal,
+                  ],
                   install : true,
                   install_dir : doc_install_dir,
                   install_tag : 'doc')
diff --git a/README.rst b/README.rst
index e2a6e275895e..e9a7dd82de74 100644
--- a/README.rst
+++ b/README.rst
@@ -67,7 +67,8 @@ for device hotplug enumeration: [optional]
         libudev-dev
 
 for documentation: [optional]
-        python3-sphinx doxygen graphviz texlive-latex-extra
+        doxygen graphviz python3-sphinx python3-sphinxcontrib.doxylink (>= 1.6.1)
+        texlive-latex-extra
 
 for gstreamer: [optional]
         libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
