Repository: mkdocs/mkdocs

Issue:
Support absolute URLs to internal pages
# Background

Version 1.0 no longer calculates filepaths back to the root. Where previously a Markdown link `[exec](/ref/exec.md)` might produce HTML with a computed path `<a href="../../../ref/exec/">exec</a>` it now simply replicates the path: `<a href="/ref/exec/">exec</a>`. While the documentation says this is unsupported (no link path should begin with a slash) this actually works fine with `mkdocs serve`. And it also works fine when the site is exported to a server root. But it fails if the site is exported to a child folder of the server root, e.g. `docs.example.com/preview/`.

# Use case

MkDocs instance code.kx.com has about 250 pages. Because from time to time major sections are re-arranged, the internal links contain no paths that include `../`. Paths are either to siblings, to descendants, or specified from the root. This allows major sections to be moved without rewriting the link filepaths. 

Currently we have 775 internal links beginning with slash, in 111 files. Upgrading to 1.0 would require us to convert all of them to relative paths. 

We would prefer to retain the paths from the site root, for the following benefits:

* Retain the mobility of large sections of our site. 
* Uncouple article copy from the site structure. For example, an article should refer to, say, a Reference article, `see [exec](/ref/exec.md)` regardless of where in the site the article is posted. 
* Avoid the work of converting the existing filepaths.
* Support our practice of previewing in development multiple branches of the main site.

# Feature request

Restore the computation of relative file paths to the root.


Discussion:
oprypin: Ready for review

Patch:
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index b6267bee..680987e2 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -382,7 +382,9 @@ Configure the strictness of MkDocs' diagnostic messages when validating links to
 
 This is a tree of configs, and for each one the value can be one of the three: `warn`, `info`, `ignore`. Which cause a logging message of the corresponding severity to be produced. The `warn` level is, of course, intended for use with `mkdocs build --strict` (where it becomes an error), which you can employ in continuous testing.
 
-> EXAMPLE: **Defaults of this config as of MkDocs 1.6:**
+The config `validation.links.absolute_links` additionally has a special value `relative_to_docs`, for [validation of absolute links](#validation-of-absolute-links).
+
+>? EXAMPLE: **Defaults of this config as of MkDocs 1.6:**
 >
 > ```yaml
 > validation:
@@ -415,7 +417,7 @@ The defaults of some of the behaviors already differ from MkDocs 1.4 and below -
 > ```yaml
 > validation:
 >   omitted_files: warn
->   absolute_links: warn
+>   absolute_links: warn  # Or 'relative_to_docs' - new in MkDocs 1.6
 >   unrecognized_links: warn
 >   anchors: warn  # New in MkDocs 1.6
 > ```
@@ -425,24 +427,46 @@ Note how in the above examples we omitted the 'nav' and 'links' keys. Here `abso
 Full list of values and examples of log messages that they can hide or make more prominent:
 
 *   `validation.nav.omitted_files`
-    * "The following pages exist in the docs directory, but are not included in the "nav" configuration: ..."
+    * > The following pages exist in the docs directory, but are not included in the "nav" configuration: ...
 *   `validation.nav.not_found`
-    * "A relative path to 'foo/bar.md' is included in the 'nav' configuration, which is not found in the documentation files."
-    * "A reference to 'foo/bar.md' is included in the 'nav' configuration, but this file is excluded from the built site."
+    * > A reference to 'foo/bar.md' is included in the 'nav' configuration, which is not found in the documentation files.
+    * > A reference to 'foo/bar.md' is included in the 'nav' configuration, but this file is excluded from the built site.
 *   `validation.nav.absolute_links`
-    * "An absolute path to '/foo/bar.html' is included in the 'nav' configuration, which presumably points to an external resource."
+    * > An absolute path to '/foo/bar.html' is included in the 'nav' configuration, which presumably points to an external resource.
 <!-- -->
 *   `validation.links.not_found`
-    * "Doc file 'example.md' contains a link '../foo/bar.md', but the target is not found among documentation files."
-    * "Doc file 'example.md' contains a link to 'foo/bar.md' which is excluded from the built site."
+    * > Doc file 'example.md' contains a link '../foo/bar.md', but the target is not found among documentation files.
+    * > Doc file 'example.md' contains a link to 'foo/bar.md' which is excluded from the built site.
 *   `validation.links.anchors`
-    * "Doc file 'example.md' contains a link '../foo/bar.md#some-heading', but the doc 'foo/bar.md' does not contain an anchor '#some-heading'."
-    * "Doc file 'example.md' contains a link '#some-heading', but there is no such anchor on this page."
+    * > Doc file 'example.md' contains a link '../foo/bar.md#some-heading', but the doc 'foo/bar.md' does not contain an anchor '#some-heading'.
+    * > Doc file 'example.md' contains a link '#some-heading', but there is no such anchor on this page.
 *   `validation.links.absolute_links`
-    * "Doc file 'example.md' contains an absolute link '/foo/bar.html', it was left as is. Did you mean 'foo/bar.md'?"
+    * > Doc file 'example.md' contains an absolute link '/foo/bar.html', it was left as is. Did you mean 'foo/bar.md'?
 *   `validation.links.unrecognized_links`
-    * "Doc file 'example.md' contains an unrecognized relative link '../foo/bar/', it was left as is. Did you mean 'foo/bar.md'?"
-    * "Doc file 'example.md' contains an unrecognized relative link 'mail\@example.com', it was left as is. Did you mean 'mailto:mail\@example.com'?"
+    * > Doc file 'example.md' contains an unrecognized relative link '../foo/bar/', it was left as is. Did you mean 'foo/bar.md'?
+    * > Doc file 'example.md' contains an unrecognized relative link 'mail\@example.com', it was left as is. Did you mean 'mailto:mail\@example.com'?
+
+#### Validation of absolute links
+
+NEW: **New in version 1.6.**
+
+> Historically, within Markdown, MkDocs only recognized **relative** links that lead to another physical `*.md` document (or media file). This is a good convention to follow because then the source pages are also freely browsable without MkDocs, for example on GitHub. Whereas absolute links were left unmodified (making them often not work as expected) or, more recently, warned against. If you dislike having to always use relative links, now you can opt into absolute links and have them work correctly.
+
+If you set the setting `validation.links.absolute_links` to the new value `relative_to_docs`, all Markdown links starting with `/` will be understood as being relative to the `docs_dir` root. The links will then be validated for correctness according to all the other rules that were already working for relative links in prior versions of MkDocs. For the HTML output, these links will still be turned relative so that the site still works reliably.
+
+So, now any document (e.g. "dir1/foo.md") can link to the document "dir2/bar.md" as `[link](/dir2/bar.md)`, in addition to the previously only correct way `[link](../dir2/bar.md)`.
+
+You have to enable the setting, though. The default is still to just skip the link.
+
+> EXAMPLE: **Settings to recognize absolute links and validate them:**
+>
+> ```yaml
+> validation:
+>   links:
+>     absolute_links: relative_to_docs
+>     anchors: warn
+>     unrecognized_links: warn
+> ```
 
 ## Build directories
 
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 9b16e302..3025045f 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -1227,19 +1227,3 @@ class PathSpec(BaseConfigOption[pathspec.gitignore.GitIgnoreSpec]):
             return pathspec.gitignore.GitIgnoreSpec.from_lines(lines=value.splitlines())
         except ValueError as e:
             raise ValidationError(str(e))
-
-
-class _LogLevel(OptionallyRequired[int]):
-    levels: Mapping[str, int] = {
-        "warn": logging.WARNING,
-        "info": logging.INFO,
-        "ignore": logging.DEBUG,
-    }
-
-    def run_validation(self, value: object) -> int:
-        if not isinstance(value, str):
-            raise ValidationError(f'Expected a string, but a {type(value)} was given.')
-        try:
-            return self.levels[value]
-        except KeyError:
-            raise ValidationError(f'Expected one of {list(self.levels)}, got {value!r}')
diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py
index 6f1da3db..68ebf3a8 100644
--- a/mkdocs/config/defaults.py
+++ b/mkdocs/config/defaults.py
@@ -1,13 +1,35 @@
 from __future__ import annotations
 
-from typing import IO, TYPE_CHECKING, Dict
+import logging
+from typing import IO, Dict, Mapping
 
 from mkdocs.config import base
 from mkdocs.config import config_options as c
+from mkdocs.structure.pages import Page, _AbsoluteLinksValidationValue
 from mkdocs.utils.yaml import get_yaml_loader, yaml_load
 
-if TYPE_CHECKING:
-    import mkdocs.structure.pages
+
+class _LogLevel(c.OptionallyRequired[int]):
+    levels: Mapping[str, int] = {
+        "warn": logging.WARNING,
+        "info": logging.INFO,
+        "ignore": logging.DEBUG,
+    }
+
+    def run_validation(self, value: object) -> int:
+        if not isinstance(value, str):
+            raise base.ValidationError(f"Expected a string, but a {type(value)} was given.")
+        try:
+            return self.levels[value]
+        except KeyError:
+            raise base.ValidationError(f"Expected one of {list(self.levels)}, got {value!r}")
+
+
+class _AbsoluteLinksValidation(_LogLevel):
+    levels: Mapping[str, int] = {
+        **_LogLevel.levels,
+        "relative_to_docs": _AbsoluteLinksValidationValue.RELATIVE_TO_DOCS,
+    }
 
 
 # NOTE: The order here is important. During validation some config options
@@ -146,37 +168,37 @@ class MkDocsConfig(base.Config):
 
     class Validation(base.Config):
         class NavValidation(base.Config):
-            omitted_files = c._LogLevel(default='info')
+            omitted_files = _LogLevel(default='info')
             """Warning level for when a doc file is never mentioned in the navigation.
             For granular configuration, see `not_in_nav`."""
 
-            not_found = c._LogLevel(default='warn')
+            not_found = _LogLevel(default='warn')
             """Warning level for when the navigation links to a relative path that isn't an existing page on the site."""
 
-            absolute_links = c._LogLevel(default='info')
+            absolute_links = _AbsoluteLinksValidation(default='info')
             """Warning level for when the navigation links to an absolute path (starting with `/`)."""
 
         nav = c.SubConfig(NavValidation)
 
         class LinksValidation(base.Config):
-            not_found = c._LogLevel(default='warn')
+            not_found = _LogLevel(default='warn')
             """Warning level for when a Markdown doc links to a relative path that isn't an existing document on the site."""
 
-            absolute_links = c._LogLevel(default='info')
+            absolute_links = _AbsoluteLinksValidation(default='info')
             """Warning level for when a Markdown doc links to an absolute path (starting with `/`)."""
 
-            unrecognized_links = c._LogLevel(default='info')
+            unrecognized_links = _LogLevel(default='info')
             """Warning level for when a Markdown doc links to a relative path that doesn't look like
             it could be a valid internal link. For example, if the link ends with `/`."""
 
-            anchors = c._LogLevel(default='info')
+            anchors = _LogLevel(default='info')
             """Warning level for when a Markdown doc links to an anchor that's not present on the target page."""
 
         links = c.SubConfig(LinksValidation)
 
     validation = c.PropagatingSubConfig[Validation]()
 
-    _current_page: mkdocs.structure.pages.Page | None = None
+    _current_page: Page | None = None
     """The currently rendered page. Please do not access this and instead
     rely on the `page` argument to event handlers."""
 
diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py
index d122c821..2c39fa20 100644
--- a/mkdocs/structure/nav.py
+++ b/mkdocs/structure/nav.py
@@ -6,7 +6,7 @@ from urllib.parse import urlsplit
 
 from mkdocs.exceptions import BuildError
 from mkdocs.structure import StructureItem
-from mkdocs.structure.pages import Page
+from mkdocs.structure.pages import Page, _AbsoluteLinksValidationValue
 from mkdocs.utils import nest_paths
 
 if TYPE_CHECKING:
@@ -164,7 +164,11 @@ def get_navigation(files: Files, config: MkDocsConfig) -> Navigation:
         scheme, netloc, path, query, fragment = urlsplit(link.url)
         if scheme or netloc:
             log.debug(f"An external link to '{link.url}' is included in the 'nav' configuration.")
-        elif link.url.startswith('/'):
+        elif (
+            link.url.startswith('/')
+            and config.validation.nav.absolute_links
+            is not _AbsoluteLinksValidationValue.RELATIVE_TO_DOCS
+        ):
             log.log(
                 config.validation.nav.absolute_links,
                 f"An absolute path to '{link.url}' is included in the 'nav' "
@@ -173,7 +177,7 @@ def get_navigation(files: Files, config: MkDocsConfig) -> Navigation:
         else:
             log.log(
                 config.validation.nav.not_found,
-                f"A relative path to '{link.url}' is included in the 'nav' "
+                f"A reference to '{link.url}' is included in the 'nav' "
                 "configuration, which is not found in the documentation files.",
             )
     return Navigation(items, pages)
@@ -195,7 +199,13 @@ def _data_to_navigation(data, files: Files, config: MkDocsConfig):
             for item in data
         ]
     title, path = data if isinstance(data, tuple) else (None, data)
-    if file := files.get_file_from_path(path):
+    lookup_path = path
+    if (
+        lookup_path.startswith('/')
+        and config.validation.nav.absolute_links is _AbsoluteLinksValidationValue.RELATIVE_TO_DOCS
+    ):
+        lookup_path = lookup_path.lstrip('/')
+    if file := files.get_file_from_path(lookup_path):
         if file.inclusion.is_excluded():
             log.log(
                 min(logging.INFO, config.validation.nav.not_found),
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 317892ac..55a0ed3d 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import copy
+import enum
 import logging
 import posixpath
 import warnings
@@ -358,7 +359,7 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor):
 
     @classmethod
     def _possible_target_uris(
-        cls, file: File, path: str, use_directory_urls: bool
+        cls, file: File, path: str, use_directory_urls: bool, suggest_absolute: bool = False
     ) -> Iterator[str]:
         """First yields the resolved file uri for the link, then proceeds to yield guesses for possible mistakes."""
         target_uri = cls._target_uri(file.src_uri, path)
@@ -397,14 +398,17 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor):
     def path_to_url(self, url: str) -> str:
         scheme, netloc, path, query, anchor = urlsplit(url)
 
+        absolute_link = None
         warning_level, warning = 0, ''
 
         # Ignore URLs unless they are a relative link to a source file.
         if scheme or netloc:  # External link.
             return url
         elif url.startswith(('/', '\\')):  # Absolute link.
-            warning_level = self.config.validation.links.absolute_links
-            warning = f"Doc file '{self.file.src_uri}' contains an absolute link '{url}', it was left as is."
+            absolute_link = self.config.validation.links.absolute_links
+            if absolute_link is not _AbsoluteLinksValidationValue.RELATIVE_TO_DOCS:
+                warning_level = absolute_link
+                warning = f"Doc file '{self.file.src_uri}' contains an absolute link '{url}', it was left as is."
         elif AMP_SUBSTITUTE in url:  # AMP_SUBSTITUTE is used internally by Markdown only for email.
             return url
         elif not path:  # Self-link containing only query or anchor.
@@ -430,7 +434,7 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor):
 
         if target_file is None and not warning:
             # Primary lookup path had no match, definitely produce a warning, just choose which one.
-            if not posixpath.splitext(path)[-1]:
+            if not posixpath.splitext(path)[-1] and absolute_link is None:
                 # No '.' in the last part of a path indicates path does not point to a file.
                 warning_level = self.config.validation.links.unrecognized_links
                 warning = (
@@ -438,7 +442,7 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor):
                     f"it was left as is."
                 )
             else:
-                target = f" '{target_uri}'" if target_uri != url else ""
+                target = f" '{target_uri}'" if target_uri != url.lstrip('/') else ""
                 warning_level = self.config.validation.links.not_found
                 warning = (
                     f"Doc file '{self.file.src_uri}' contains a link '{url}', "
@@ -456,6 +460,8 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor):
                     if self.files.get_file_from_path(path) is not None:
                         if anchor and path == self.file.src_uri:
                             path = ''
+                        elif absolute_link is _AbsoluteLinksValidationValue.RELATIVE_TO_DOCS:
+                            path = '/' + path
                         else:
                             path = utils.get_relative_url(path, self.file.src_uri)
                         suggest_url = urlunsplit(('', '', path, query, anchor))
@@ -545,3 +551,7 @@ class _ExtractTitleTreeprocessor(markdown.treeprocessors.Treeprocessor):
     def _register(self, md: markdown.Markdown) -> None:
         self.postprocessors = tuple(md.postprocessors)
         md.treeprocessors.register(self, "mkdocs_extract_title", priority=-1)  # After the end.
+
+
+class _AbsoluteLinksValidationValue(enum.IntEnum):
+    RELATIVE_TO_DOCS = -1


Test patch:
diff --git a/mkdocs/tests/structure/nav_tests.py b/mkdocs/tests/structure/nav_tests.py
index 04846111..11b7464f 100644
--- a/mkdocs/tests/structure/nav_tests.py
+++ b/mkdocs/tests/structure/nav_tests.py
@@ -131,6 +131,44 @@ class SiteNavigationTests(unittest.TestCase):
         self.assertEqual(len(site_navigation.items), 3)
         self.assertEqual(len(site_navigation.pages), 1)
 
+    def test_nav_absolute_links_with_validation(self):
+        nav_cfg = [
+            {'Home': 'index.md'},
+            {'Local page': '/foo/bar.md'},
+            {'Local link': '/local.md'},
+            {'External': 'http://example.com/external.html'},
+        ]
+        expected = dedent(
+            """
+            Page(title='Home', url='/')
+            Page(title='Local page', url='/foo/bar/')
+            Link(title='Local link', url='/local.md')
+            Link(title='External', url='http://example.com/external.html')
+            """
+        )
+        cfg = load_config(
+            nav=nav_cfg,
+            site_url='http://example.com/',
+            validation=dict(nav=dict(absolute_links='relative_to_docs')),
+        )
+        fs = [
+            File('index.md', cfg.docs_dir, cfg.site_dir, cfg.use_directory_urls),
+            File('foo/bar.md', cfg.docs_dir, cfg.site_dir, cfg.use_directory_urls),
+        ]
+        files = Files(fs)
+        with self.assertLogs('mkdocs', level='DEBUG') as cm:
+            site_navigation = get_navigation(files, cfg)
+        self.assertEqual(
+            cm.output,
+            [
+                "WARNING:mkdocs.structure.nav:A reference to '/local.md' is included in the 'nav' configuration, which is not found in the documentation files.",
+                "DEBUG:mkdocs.structure.nav:An external link to 'http://example.com/external.html' is included in the 'nav' configuration.",
+            ],
+        )
+        self.assertEqual(str(site_navigation).strip(), expected)
+        self.assertEqual(len(site_navigation.items), 4)
+        self.assertEqual(len(site_navigation.pages), 2)
+
     def test_nav_bad_links(self):
         nav_cfg = [
             {'Home': 'index.md'},
@@ -152,8 +190,8 @@ class SiteNavigationTests(unittest.TestCase):
         self.assertEqual(
             cm.output,
             [
-                "WARNING:mkdocs.structure.nav:A relative path to 'missing.html' is included in the 'nav' configuration, which is not found in the documentation files.",
-                "WARNING:mkdocs.structure.nav:A relative path to 'example.com' is included in the 'nav' configuration, which is not found in the documentation files.",
+                "WARNING:mkdocs.structure.nav:A reference to 'missing.html' is included in the 'nav' configuration, which is not found in the documentation files.",
+                "WARNING:mkdocs.structure.nav:A reference to 'example.com' is included in the 'nav' configuration, which is not found in the documentation files.",
             ],
         )
         self.assertEqual(str(site_navigation).strip(), expected)
diff --git a/mkdocs/tests/structure/page_tests.py b/mkdocs/tests/structure/page_tests.py
index f24078bd..22b46941 100644
--- a/mkdocs/tests/structure/page_tests.py
+++ b/mkdocs/tests/structure/page_tests.py
@@ -1003,6 +1003,48 @@ class RelativePathExtensionTests(unittest.TestCase):
             '<a href="./#test">link</a>',
         )
 
+    def test_absolute_self_anchor_link_with_suggestion(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                content='[link](/index#test)',
+                files=['index.md'],
+                logs="INFO:Doc file 'index.md' contains an absolute link '/index#test', it was left as is. Did you mean '#test'?",
+            ),
+            '<a href="/index#test">link</a>',
+        )
+
+    def test_absolute_self_anchor_link_with_validation_and_suggestion(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[link](/index#test)',
+                files=['index.md'],
+                logs="WARNING:Doc file 'index.md' contains a link '/index#test', but the target 'index' is not found among documentation files. Did you mean '#test'?",
+            ),
+            '<a href="/index#test">link</a>',
+        )
+
+    def test_absolute_anchor_link_with_validation(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[link](/foo/bar.md#test)',
+                files=['index.md', 'foo/bar.md'],
+            ),
+            '<a href="foo/bar/#test">link</a>',
+        )
+
+    def test_absolute_anchor_link_with_validation_and_suggestion(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[link](/foo/bar#test)',
+                files=['zoo/index.md', 'foo/bar.md'],
+                logs="WARNING:Doc file 'zoo/index.md' contains a link '/foo/bar#test', but the target 'foo/bar' is not found among documentation files. Did you mean '/foo/bar.md#test'?",
+            ),
+            '<a href="/foo/bar#test">link</a>',
+        )
+
     def test_external_link(self):
         self.assertEqual(
             self.get_rendered_result(
@@ -1038,7 +1080,58 @@ class RelativePathExtensionTests(unittest.TestCase):
             '<a href="/path/to/file">absolute link</a>',
         )
 
-    def test_absolute_link(self):
+    def test_absolute_link_with_validation(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[absolute link](/path/to/file.md)',
+                files=['index.md', 'path/to/file.md'],
+            ),
+            '<a href="path/to/file/">absolute link</a>',
+        )
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                use_directory_urls=False,
+                content='[absolute link](/path/to/file.md)',
+                files=['path/index.md', 'path/to/file.md'],
+            ),
+            '<a href="to/file.html">absolute link</a>',
+        )
+
+    def test_absolute_link_with_validation_and_suggestion(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                use_directory_urls=False,
+                content='[absolute link](/path/to/file/)',
+                files=['path/index.md', 'path/to/file.md'],
+                logs="WARNING:Doc file 'path/index.md' contains a link '/path/to/file/', but the target 'path/to/file' is not found among documentation files.",
+            ),
+            '<a href="/path/to/file/">absolute link</a>',
+        )
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[absolute link](/path/to/file)',
+                files=['path/index.md', 'path/to/file.md'],
+                logs="WARNING:Doc file 'path/index.md' contains a link '/path/to/file', but the target is not found among documentation files. Did you mean '/path/to/file.md'?",
+            ),
+            '<a href="/path/to/file">absolute link</a>',
+        )
+
+    def test_absolute_link_with_validation_just_slash(self):
+        self.assertEqual(
+            self.get_rendered_result(
+                validation=dict(links=dict(absolute_links='relative_to_docs')),
+                content='[absolute link](/)',
+                files=['path/to/file.md', 'index.md'],
+                logs="WARNING:Doc file 'path/to/file.md' contains a link '/', but the target '.' is not found among documentation files. Did you mean '/index.md'?",
+            ),
+            '<a href="/">absolute link</a>',
+        )
+
+    def test_absolute_link_preserved_and_warned(self):
         self.assertEqual(
             self.get_rendered_result(
                 validation=dict(links=dict(absolute_links='warn')),