diff --git a/Documentation/conf.py.in b/Documentation/conf.py.in
index 34fa3956f49e..5e0a7cff2984 100644
--- a/Documentation/conf.py.in
+++ b/Documentation/conf.py.in
@@ -9,14 +9,8 @@
 
 # -- Path setup --------------------------------------------------------------
 
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
-
+import sys
+sys.path.insert(0, "@CURRENT_SRCDIR@/extensions")
 
 # -- Project information -----------------------------------------------------
 
@@ -39,6 +33,7 @@ author = 'The libcamera documentation authors'
 extensions = [
     'sphinx.ext.graphviz',
     'sphinxcontrib.doxylink',
+    'doxygen-link',
 ]
 
 graphviz_output_format = 'svg'
@@ -75,14 +70,25 @@ pygments_style = None
 doxylink = {
     'doxy-pub': (
         '@TOP_BUILDDIR@/Documentation/api-html/tagfile.xml',
-        '../api-html/',
+        'api-html/',
     ),
     'doxy-int': (
         '@TOP_BUILDDIR@/Documentation/internal-api-html/tagfile.xml',
-        '../internal-api-html/',
+        'internal-api-html/',
     ),
 }
 
+doxygen_links = [
+    [
+        '@TOP_BUILDDIR@/Documentation/html/api-html',
+        '@TOP_BUILDDIR@/Documentation/api-html',
+    ],
+    [
+        '@TOP_BUILDDIR@/Documentation/html/internal-api-html',
+        '@TOP_BUILDDIR@/Documentation/internal-api-html',
+    ],
+]
+
 # -- Options for HTML output -------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
diff --git a/Documentation/extensions/doxygen-link.py b/Documentation/extensions/doxygen-link.py
new file mode 100644
index 000000000000..8b94a4dd7f0b
--- /dev/null
+++ b/Documentation/extensions/doxygen-link.py
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2025, Ideas on Board Oy
+
+import contextlib
+import os
+import shutil
+from pathlib import Path
+from sphinx.util import logging
+
+__version__ = "0.0.0"
+
+logger = logging.getLogger(__name__)
+
+
+def on_config_inited(app, config):
+    entries = []
+
+    outdir = Path(app.outdir).absolute()
+
+    for index, items in enumerate(config.doxygen_links):
+        err_msg_prefix = f'Config variable `doxygen_links` contains invalid entry {index} (`{items}`)'
+
+        if len(items) != 2:
+            raise ValueError(f'{err_msg_prefix}: expected (path, target)')
+
+        path = Path(items[0]).absolute()
+        target = Path(items[1]).relative_to(path.parent, walk_up=True)
+
+        if not path.is_relative_to(outdir):
+            raise ValueError(f'{err_msg_prefix}: path `{items[0]}` is outside of output directory `{outdir}`')
+
+        entries.append([path, target])
+
+    config.doxygen_links = entries
+
+
+def on_builder_inited(app):
+    # Remove the symlinks if they exist, to avoid overwriting the index.html
+    # generated by Doxygen with the placeholder index from Sphinx.
+
+    for path, target in app.config.doxygen_links:
+        if path.is_symlink():
+            logger.info(f'Removing existing symlink {path}')
+            os.unlink(path)
+
+
+def on_build_finished(app, exc):
+    # Create the symlinks. Remove any existing placeholder directory
+    # recursively first.
+
+    if exc:
+        return
+
+    for path, target in app.config.doxygen_links:
+        logger.info(f'Creating symlink {path} -> {target}')
+
+        if path.is_dir():
+            shutil.rmtree(path)
+
+        os.symlink(target, path)
+
+
+def setup(app):
+    app.add_config_value('doxygen_links', [], 'env', frozenset({list, tuple}))
+    app.connect('config-inited', on_config_inited)
+    app.connect('builder-inited', on_builder_inited)
+    app.connect('build-finished', on_build_finished)
+
+    return {
+        "version": __version__,
+        "parallel_read_safe": True,
+        "parallel_write_safe": True,
+    }
diff --git a/Documentation/meson.build b/Documentation/meson.build
index f73407432fff..82d76b257b8c 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -12,6 +12,7 @@ dot = find_program('dot', required : get_option('documentation'))
 if doxygen.found() and dot.found()
     cdata = configuration_data()
     cdata.set('VERSION', 'v@0@'.format(libcamera_git_version))
+    cdata.set('CURRENT_SRCDIR', meson.current_source_dir())
     cdata.set('TOP_SRCDIR', meson.project_source_root())
     cdata.set('TOP_BUILDDIR', meson.project_build_root())
     cdata.set('OUTPUT_DIR', meson.current_build_dir())
@@ -89,7 +90,7 @@ if doxygen.found() and dot.found()
                                    output : 'api-html',
                                    command : [doxygen, doxyfile],
                                    install : true,
-                                   install_dir : doc_install_dir,
+                                   install_dir : doc_install_dir / 'html',
                                    install_tag : 'doc')
 
     # This is the internal documentation, which hard-codes a list of directories
@@ -109,7 +110,7 @@ if doxygen.found() and dot.found()
                                      output : 'internal-api-html',
                                      command : [doxygen, doxyfile],
                                      install : true,
-                                     install_dir : doc_install_dir,
+                                     install_dir : doc_install_dir / 'html',
                                      install_tag : 'doc-internal')
 endif
 
@@ -149,7 +150,11 @@ if sphinx.found()
     fs = import('fs')
     sphinx_conf_dir = fs.parent(sphinx_conf)
 
+    sphinx_env = environment()
+    sphinx_env.set('PYTHONDONTWRITEBYTECODE', '1')
+
     docs_sources = [
+        'api-html/index.rst',
         'camera-sensor-model.rst',
         'code-of-conduct.rst',
         'coding-style.rst',
@@ -164,6 +169,7 @@ if sphinx.found()
         'guides/pipeline-handler.rst',
         'guides/tracing.rst',
         'index.rst',
+        'internal-api-html/index.rst',
         'introduction.rst',
         'lens_driver_requirements.rst',
         'libcamera_architecture.rst',
@@ -183,10 +189,14 @@ if sphinx.found()
                   input : docs_sources,
                   output : 'html',
                   build_by_default : true,
+                  depend_files : [
+                      'extensions/doxygen-link.py',
+                  ],
                   depends : [
                       doxygen_public,
                       doxygen_internal,
                   ],
+                  env : sphinx_env,
                   install : true,
                   install_dir : doc_install_dir,
                   install_tag : 'doc')
@@ -195,7 +205,8 @@ if sphinx.found()
                   command : [sphinx, '-W', '-b', 'linkcheck',
                              '-c', sphinx_conf_dir,
                              meson.current_source_dir(), '@OUTPUT@'],
-                  build_always_stale : true,
                   input : docs_sources,
-                  output : 'linkcheck')
+                  output : 'linkcheck',
+                  build_always_stale : true,
+                  env : sphinx_env)
 endif
