From fb0d18ee0b5cff8781c9271772d10d40cabb10e1 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 16 May 2026 12:03:46 +0200 Subject: [PATCH] testers.lycheeLinkCheck: improve root-relative link UX Lychee by itself does not have a way to explicitly declare the intent for the site to be relocatable. Recent-ish changes in lychee and/or its dependencies have made it catch the a root reference in all mdbook 404.html pages, which is quite confusing. By improving the UX around this aspect of the site, we resolve at least part of that confusion. (Though mdbook users should probably exclude the 404 page) --- doc/build-helpers/testers.chapter.md | 8 ++++ pkgs/build-support/testers/lychee.nix | 39 ++++++++++++++++++- pkgs/by-name/ly/lychee/package.nix | 1 + .../ly/lychee/tests/fail-rootRelative.nix | 21 ++++++++++ pkgs/by-name/ly/lychee/tests/ok.nix | 2 + 5 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 pkgs/by-name/ly/lychee/tests/fail-rootRelative.nix diff --git a/doc/build-helpers/testers.chapter.md b/doc/build-helpers/testers.chapter.md index 632b11ab12ea..5de01694e718 100644 --- a/doc/build-helpers/testers.chapter.md +++ b/doc/build-helpers/testers.chapter.md @@ -98,6 +98,14 @@ It has two modes: : The path to the files to check. +`relocatable` (boolean, optional) {#tester-lycheeLinkCheck-param-relocatable} + +: Whether the site is expected to be relocatable, i.e. servable from any URL path prefix. + + When `true` (the default), root-relative links (starting with `/`) are treated as errors, because they break when the site is served from a subpath or opened via `file://` URLs. + + When `false`, root-relative links are resolved against the `site` directory. + `remap` (attribute set, optional) {#tester-lycheeLinkCheck-param-remap} : An attribute set where the attribute names are regular expressions. diff --git a/pkgs/build-support/testers/lychee.nix b/pkgs/build-support/testers/lychee.nix index 493ef3e880f9..eb2ddef13152 100644 --- a/pkgs/build-support/testers/lychee.nix +++ b/pkgs/build-support/testers/lychee.nix @@ -7,7 +7,7 @@ deps@{ writeShellApplication, }: let - inherit (lib) mapAttrsToList throwIf; + inherit (lib) mapAttrsToList optionalString throwIf; inherit (lib.strings) hasInfix hasPrefix escapeNixString; toURL = @@ -34,6 +34,7 @@ let lycheeLinkCheck = { site, + relocatable ? null, remap ? { }, lychee ? deps.lychee, extraConfig ? { }, @@ -55,6 +56,11 @@ let inherit lychee remap; config = { include_fragments = "full"; + root_dir = + if relocatable == false then + finalAttrs.site + else + "/root-relative-links-are-forbidden-use-relative-links"; } // lib.optionalAttrs (finalAttrs.passthru.remap != { }) { remap = mapAttrsToList ( @@ -62,6 +68,9 @@ let ) finalAttrs.passthru.remap; } // extraConfig; + # NOTE: The online wrapper does not implement the relocatable hint message. + # It uses the same configFile (with the fake root_dir), so root-relative + # links still fail, but without the custom explanation. online = writeShellApplication { name = "run-lychee-online"; runtimeInputs = [ finalAttrs.passthru.lychee ]; @@ -77,7 +86,33 @@ let }; buildCommand = '' echo Checking internal links on $site - lychee --offline --config $configFile "''${extraArgs[@]}" $site + rc=0 + lychee --offline --config $configFile "''${extraArgs[@]}" $site 2>&1 | tee lychee.log || rc="''${PIPESTATUS[0]}" + ${optionalString (relocatable != false) '' + if [ "$rc" -ne 0 ] && grep -qF "[ERROR] file:///root-relative-links-are-forbidden" lychee.log; then + echo + ${ + if relocatable == null then + '' + echo "❄️ ⚠️ Your site contains root-relative links (starting with '/')." + echo "Please set the relocatable parameter in lycheeLinkCheck:" + echo " - relocatable = true: root-relative links are forbidden because they" + echo " break when the site is served from a subpath or opened via file:// URLs." + echo " - relocatable = false: root-relative links are allowed because the" + echo " site is always served from the root." + '' + else + '' + echo "❄️ ⚠️ Root-relative links (starting with '/') are not allowed because this" + echo "site is marked as relocatable (relocatable = true)." + '' + } + echo "See https://nixos.org/manual/nixpkgs/unstable/#tester-lycheeLinkCheck-param-relocatable" + fi + ''} + if [ "$rc" -ne 0 ]; then + exit "$rc" + fi touch $out ''; }); diff --git a/pkgs/by-name/ly/lychee/package.nix b/pkgs/by-name/ly/lychee/package.nix index 523513c1e1e5..97422a46de71 100644 --- a/pkgs/by-name/ly/lychee/package.nix +++ b/pkgs/by-name/ly/lychee/package.nix @@ -84,6 +84,7 @@ rustPlatform.buildRustPackage (finalAttrs: { ok = callPackage ./tests/ok.nix { }; fail = callPackage ./tests/fail.nix { }; fail-emptyDirectory = callPackage ./tests/fail-emptyDirectory.nix { }; + fail-rootRelative = callPackage ./tests/fail-rootRelative.nix { }; network = testers.runNixOSTest ./tests/network.nix; }; diff --git a/pkgs/by-name/ly/lychee/tests/fail-rootRelative.nix b/pkgs/by-name/ly/lychee/tests/fail-rootRelative.nix new file mode 100644 index 000000000000..3fa5defac3ee --- /dev/null +++ b/pkgs/by-name/ly/lychee/tests/fail-rootRelative.nix @@ -0,0 +1,21 @@ +{ runCommand, testers }: +let + sitePkg = runCommand "site" { } '' + dist=$out/dist + mkdir -p $dist + echo "hi" > $dist/index.html + echo "home" > $dist/page.html + ''; + + linkCheck = testers.lycheeLinkCheck { + site = sitePkg + "/dist"; + }; + + failure = testers.testBuildFailure linkCheck; + +in +runCommand "link-check-fail" { inherit failure; } '' + grep -F "root-relative-links-are-forbidden-use-relative-links/index.html" $failure/testBuildFailure.log >/dev/null + grep -F "Please set the relocatable parameter" $failure/testBuildFailure.log >/dev/null + touch $out +'' diff --git a/pkgs/by-name/ly/lychee/tests/ok.nix b/pkgs/by-name/ly/lychee/tests/ok.nix index b93b93384ba1..e2eb4f9f939f 100644 --- a/pkgs/by-name/ly/lychee/tests/ok.nix +++ b/pkgs/by-name/ly/lychee/tests/ok.nix @@ -5,10 +5,12 @@ let mkdir -p $dist echo "foobar" > $dist/index.html echo "index" > $dist/foo.html + echo "home" > $dist/root-relative.html ''; in testers.lycheeLinkCheck rec { site = sitePkg + "/dist"; + relocatable = false; remap = { "https://example.com" = site; };