From 3079dd0ea66928a76971fbc8b75c557e40b9db3f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:14:31 +0000 Subject: [PATCH 1/6] Validate config in CI, and repair two dead Renovate custom managers Both customManagers in renovate.json matched no file at all. Their managerFilePatterns were written as bare regexes: "^\\.github/workflows/.*\\.ya?ml$" managerFilePatterns takes "regex or glob", and a value only counts as a regex when it is wrapped in slashes. Undelimited, that string is read as a glob and matches nothing, so neither manager has ever fired and HUGO_VERSION has not been offered an update. The visible symptom: Hugo is pinned here at 0.163.1 while Hagmolenpups.nl, whose patterns are correctly slashed, is on 0.164.0. Both patterns are now delimited, and matching is verified against the real workflow filenames. Nothing reported this. renovate-config-validator calls the config valid, because it is -- it just does nothing. So the new Config validation workflow checks for it directly, alongside Renovate's own validator and schema validation of dependabot.yml (absent here, so that step reports and passes). The workflow also runs actionlint. The other repositories in the organisation run it from their quality workflow; this one had no equivalent, so the workflow files went unchecked. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/scripts/check-renovate-patterns.py | 74 +++++++++ .github/workflows/config-validation.yml | 120 ++++++++++++++ renovate.json | 180 ++++++++++++--------- 3 files changed, 295 insertions(+), 79 deletions(-) create mode 100755 .github/scripts/check-renovate-patterns.py create mode 100644 .github/workflows/config-validation.yml diff --git a/.github/scripts/check-renovate-patterns.py b/.github/scripts/check-renovate-patterns.py new file mode 100755 index 0000000..bb7c695 --- /dev/null +++ b/.github/scripts/check-renovate-patterns.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +"""Flag Renovate file patterns that look like a regex but are not delimited. + +managerFilePatterns and matchFileNames accept "RegEx (re2) and glob patterns". +A value counts as a regex only when it is wrapped in slashes; everything else +is read as a glob. So a pattern like + + "^\\.github/workflows/.*\\.ya?ml$" + +matches no file at all, and the custom manager around it never fires. Nothing +reports this: renovate-config-validator says the config is valid, because it +is -- it just silently does nothing. The only visible symptom is a dependency +that stops receiving updates, which is easy to miss for months. + +Usage: check-renovate-patterns.py [config.json ...] +Missing files are skipped, so the same call works in every repository. +""" +import json +import pathlib +import re +import sys + +# Constructs that carry meaning in a regex but not in a glob. +REGEXY = re.compile(r"^\^|\$$|\\\.|\.\*|\.\+|\(\?|\[\^|\\d|\\w|\\s|[)?]\|") + +# Renovate options whose values are matched as "regex or glob". +PATTERN_KEYS = { + "managerFilePatterns", + "matchFileNames", + "fileMatch", + "matchPackageNames", +} + +problems = [] + + +def walk(node, path, source): + if isinstance(node, dict): + for key, value in node.items(): + if key in PATTERN_KEYS and isinstance(value, list): + for index, pattern in enumerate(value): + if not isinstance(pattern, str): + continue + # A trailing "i" flag is allowed: /pattern/i + delimited = pattern.startswith("/") and pattern.rstrip("i").endswith("/") + if REGEXY.search(pattern) and not delimited: + problems.append((source, f"{path}.{key}[{index}]", pattern)) + walk(value, f"{path}.{key}", source) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]", source) + + +files = [path for path in (pathlib.Path(a) for a in sys.argv[1:]) if path.is_file()] +if not files: + print("No Renovate config found to check.") + sys.exit(0) + +for config in files: + walk(json.loads(config.read_text()), "$", str(config)) + +if problems: + print("Renovate file patterns that look like a regex but are not wrapped in slashes.") + print("Renovate reads these as globs, so they match nothing and the rule never fires.\n") + for source, where, pattern in problems: + print(f"::error file={source}::{where}: {pattern!r} is read as a glob, not a regex") + print(f" {source} {where}") + print(f" found: {pattern!r}") + print(f" expect: '/{pattern}/'\n") + sys.exit(1) + +print(f"Checked {len(files)} Renovate config file(s): all file patterns are well formed.") diff --git a/.github/workflows/config-validation.yml b/.github/workflows/config-validation.yml new file mode 100644 index 0000000..6b630ec --- /dev/null +++ b/.github/workflows/config-validation.yml @@ -0,0 +1,120 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Config validation + +# The bot configs are the one part of CI that nothing else exercises: a broken +# renovate.json or dependabot.yml does not fail a build, it just quietly stops +# doing its job. This workflow is the thing that notices. + +on: + push: + branches: [main, development] + paths: + - 'renovate.json' + - '.github/renovate.json' + - '.github/dependabot.yml' + - '.github/dependabot.yaml' + - '.github/scripts/check-renovate-patterns.py' + # Broader than the other repos: the actionlint job below covers every + # workflow, so every workflow change is relevant here. + - '.github/workflows/**' + pull_request: + branches: [main, development] + paths: + - 'renovate.json' + - '.github/renovate.json' + - '.github/dependabot.yml' + - '.github/dependabot.yaml' + - '.github/scripts/check-renovate-patterns.py' + # Broader than the other repos: the actionlint job below covers every + # workflow, so every workflow change is relevant here. + - '.github/workflows/**' + workflow_dispatch: + +permissions: {} + +jobs: + bot-configs: + name: Renovate and Dependabot config + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 'lts/*' + + # Renovate's own validator. --strict also fails on warnings, such as an + # option that is valid but deprecated. Given no arguments it finds the + # config files itself and validates them as repository config; passing a + # path instead makes it validate them as global config, which is a + # different and weaker set of rules. + - name: Validate Renovate config + env: + # renovate: datasource=npm depName=renovate + RENOVATE_VERSION: "42.99.0" + run: npx --yes --package "renovate@${RENOVATE_VERSION}" -- renovate-config-validator --strict + + # The validator above accepts a well-formed pattern that matches nothing, + # so this covers the gap it leaves. + - name: Check Renovate file patterns + run: python3 .github/scripts/check-renovate-patterns.py renovate.json .github/renovate.json + + # GitHub validates dependabot.yml only after it is on the default branch, + # and reports the result on a tab nobody opens. This brings that forward + # to the pull request. + - name: Validate Dependabot config + env: + # renovate: datasource=pypi depName=check-jsonschema + CHECK_JSONSCHEMA_VERSION: "0.38.0" + run: | + config="" + for candidate in .github/dependabot.yml .github/dependabot.yaml; do + if [ -f "$candidate" ]; then + config="$candidate" + break + fi + done + if [ -z "$config" ]; then + echo "No dependabot.yml in this repository; nothing to validate." + exit 0 + fi + pipx install "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}" + check-jsonschema --builtin-schema vendor.dependabot "$config" + + # The workflow files are config too. The other repositories in the + # organisation run actionlint from their quality workflow; this one had no + # equivalent, so it lives here. + workflow-lint: + name: Check workflow files + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Pinned release plus checksum, rather than piping a script from a + # branch straight into bash. + - name: Install actionlint + env: + # renovate: datasource=github-releases depName=rhysd/actionlint + ACTIONLINT_VERSION: "1.7.12" + ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" + run: | + curl -sSL --fail-with-body -o actionlint.tar.gz \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c - + tar -xzf actionlint.tar.gz actionlint + sudo install -m 0755 actionlint /usr/local/bin/actionlint + + - name: Run actionlint + run: actionlint -color diff --git a/renovate.json b/renovate.json index 917e300..2c95a90 100644 --- a/renovate.json +++ b/renovate.json @@ -1,80 +1,102 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "timezone": "Europe/Amsterdam", - "forkProcessing": "enabled", - "pinDigests": true, - "assigneesFromCodeOwners": true, - "reviewersFromCodeOwners": true, - "enabledManagers": [ - "github-actions", - "gomod", - "custom.regex" - ], - "prHourlyLimit": 2, - "prConcurrentLimit": 5, - "labels": [ - "dependencies" - ], - "packageRules": [ - { - "matchManagers": [ - "dockerfile", - "docker-compose" - ], - "groupName": "Docker images", - "addLabels": [ - "docker" - ] - }, - { - "matchManagers": [ - "github-actions" - ], - "groupName": "GitHub Actions", - "addLabels": [ - "github-actions" - ] - }, - { - "matchManagers": [ - "gomod" - ], - "groupName": "Go modules", - "addLabels": [ - "go" - ] - } - ], - "customManagers": [ - { - "customType": "regex", - "managerFilePatterns": [ - "^\\.github/workflows/.*\\.ya?ml$" - ], - "matchStrings": [ - "HUGO_VERSION:\\s*(?\\d+\\.\\d+\\.\\d+)" - ], - "depNameTemplate": "gohugoio/hugo", - "datasourceTemplate": "github-releases", - "versioningTemplate": "semver" - }, - { - "customType": "regex", - "managerFilePatterns": [ - "^\\.github/workflows/.*\\.ya?ml$" - ], - "matchStrings": [ - "hugo-version:\\s*['\"]?(?\\d+\\.\\d+\\.\\d+)['\"]?" - ], - "depNameTemplate": "gohugoio/hugo", - "datasourceTemplate": "github-releases", - "versioningTemplate": "semver" - } - ], - "automerge": true, - "automergeType": "pr", - "semanticCommits": "enabled" +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "timezone": "Europe/Amsterdam", + "forkProcessing": "enabled", + "pinDigests": true, + "assigneesFromCodeOwners": true, + "reviewersFromCodeOwners": true, + "enabledManagers": [ + "github-actions", + "gomod", + "custom.regex" + ], + "prHourlyLimit": 2, + "prConcurrentLimit": 5, + "labels": [ + "dependencies" + ], + "packageRules": [ + { + "matchManagers": [ + "dockerfile", + "docker-compose" + ], + "groupName": "Docker images", + "addLabels": [ + "docker" + ] + }, + { + "matchManagers": [ + "github-actions" + ], + "groupName": "GitHub Actions", + "addLabels": [ + "github-actions" + ] + }, + { + "matchManagers": [ + "gomod" + ], + "groupName": "Go modules", + "addLabels": [ + "go" + ] + }, + { + "description": "Versions pinned by hand in the workflows. Not automerged: actionlint is pinned alongside a checksum that has to be updated in the same PR.", + "matchManagers": [ + "custom.regex" + ], + "groupName": "Build tooling versions", + "addLabels": [ + "build-tooling" + ], + "automerge": false + } + ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.github/workflows/.*\\.ya?ml$/" + ], + "matchStrings": [ + "HUGO_VERSION:\\s*(?\\d+\\.\\d+\\.\\d+)" + ], + "depNameTemplate": "gohugoio/hugo", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.github/workflows/.*\\.ya?ml$/" + ], + "matchStrings": [ + "hugo-version:\\s*['\"]?(?\\d+\\.\\d+\\.\\d+)['\"]?" + ], + "depNameTemplate": "gohugoio/hugo", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Tool versions pinned in workflows, annotated with a `# renovate:` comment on the line above", + "managerFilePatterns": [ + "/^\\.github/workflows/.*\\.ya?ml$/" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-]+) depName=(?\\S+)\\s+[A-Za-z_]+: \"(?[^\"]+)\"" + ], + "extractVersionTemplate": "^v?(?.*)$" + } + ], + "automerge": true, + "automergeType": "pr", + "semanticCommits": "enabled" } From dad5aba7e8a643ea9b589f9d6deeee62d0edc4ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:17:12 +0000 Subject: [PATCH 2/6] Quote the Hugo download URL in the two install steps Both Install Hugo steps left ${HUGO_VERSION} unquoted inside the download URL, which shellcheck reports as SC2086. The new actionlint job runs shellcheck over every run: block and fails on it. Harmless in practice, since the value is a version number with no spaces or glob characters in it. Quoting is still the right shape, and it keeps the workflow files clean under the linter that now watches them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/workflows/hugo.yml | 6 +++--- .github/workflows/pr-checks.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index 9d219b5..800a8af 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -36,9 +36,9 @@ jobs: - name: Install Hugo run: | - wget -O ${{ runner.temp }}/hugo.deb \ - https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ - && sudo dpkg -i ${{ runner.temp }}/hugo.deb + wget -O "${{ runner.temp }}/hugo.deb" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" \ + && sudo dpkg -i "${{ runner.temp }}/hugo.deb" - name: Setup Pages id: pages diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 53bbd92..f2543e1 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -163,9 +163,9 @@ jobs: go-version-file: src/go.mod - name: Install Hugo run: | - wget -O ${{ runner.temp }}/hugo.deb \ - https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ - && sudo dpkg -i ${{ runner.temp }}/hugo.deb + wget -O "${{ runner.temp }}/hugo.deb" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" \ + && sudo dpkg -i "${{ runner.temp }}/hugo.deb" - name: Build env: HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache From 9e5c4f549cab676dd33d1f65ddaa0e89eec5f59f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:19:39 +0000 Subject: [PATCH 3/6] Retry the actionlint download GitHub's release CDN hands out an occasional 503, which failed a job in THectic.nl on this same download pattern. curl already retries 5xx and timeouts when asked; --retry-all-errors extends that to connection failures. The checksum verification is unaffected: a retry only fetches the archive again, and it still has to match the pinned SHA-256. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/workflows/config-validation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/config-validation.yml b/.github/workflows/config-validation.yml index 6b630ec..d6bd757 100644 --- a/.github/workflows/config-validation.yml +++ b/.github/workflows/config-validation.yml @@ -111,6 +111,7 @@ jobs: ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" run: | curl -sSL --fail-with-body -o actionlint.tar.gz \ + --retry 5 --retry-delay 3 --retry-all-errors \ "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c - tar -xzf actionlint.tar.gz actionlint From de20199b1118ae6817ef999b0c621be5e3bd4bb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:37:18 +0000 Subject: [PATCH 4/6] fix: install lychee ourselves instead of via lychee-action lycheeverse/lychee-action fetches its binary with a bare `curl -sfLO`: no retry, and no check on what comes back. That download failed twice in a quarter of an hour across this organisation while GitHub's release CDN was struggling, once in this job and once in THectic.nl, and in both cases the job gave up without having checked a single link. Replaced with a pinned version, a checksum verified against the release's own lychee-x86_64-unknown-linux-gnu.tar.gz.sha256, and a retried download. As a side effect the binary is now verified at all, which the action never did. The Renovate annotation needs an extractVersion here: lychee tags its releases "lychee-v0.24.2" rather than "v0.24.2", so the default pattern cannot read a version out of the tag and would never offer an update. The shared matchString now takes an optional extractVersion= field for this, and falls back to the default when it is absent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/workflows/pr-checks.yml | 33 +++++++++++++++++++++++++-------- renovate.json | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index f2543e1..fe15381 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -189,15 +189,32 @@ jobs: with: name: hugo-public path: public/ + # Installed by hand rather than through lycheeverse/lychee-action. That + # action fetches its binary with a bare `curl -sfLO`: no retry, and no + # check on what comes back. This job and one in THectic.nl both failed on + # that download within a quarter of an hour when GitHub's release CDN was + # having a bad day, neither having checked a single link. Pinned version, + # verified checksum, retried download. + - name: Install lychee + env: + # extractVersion: lychee tags its releases "lychee-v0.24.2", not + # "v0.24.2", so the default pattern cannot read the version out. + # renovate: datasource=github-releases depName=lycheeverse/lychee extractVersion=^lychee-v(?.+)$ + LYCHEE_VERSION: "0.24.2" + # From the release's own lychee-x86_64-unknown-linux-gnu.tar.gz.sha256 + LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + run: | + curl -sSL --fail-with-body -o lychee.tar.gz \ + --retry 5 --retry-delay 3 --retry-all-errors \ + "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" + echo "${LYCHEE_SHA256} lychee.tar.gz" | sha256sum -c - + tar -xzf lychee.tar.gz lychee-x86_64-unknown-linux-gnu/lychee + sudo install -m 0755 lychee-x86_64-unknown-linux-gnu/lychee /usr/local/bin/lychee + lychee --version + - name: Check internal links - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 - with: - args: >- - --offline - --include-fragments - --root-dir ./public - public/**/*.html - fail: true + run: | + lychee --offline --include-fragments --root-dir ./public "public/**/*.html" # ── 8. Auto-tick PR checklist ──────────────────────────────────────────────── update-checklist: diff --git a/renovate.json b/renovate.json index 2c95a90..94c4088 100644 --- a/renovate.json +++ b/renovate.json @@ -91,7 +91,7 @@ "/^\\.github/workflows/.*\\.ya?ml$/" ], "matchStrings": [ - "# renovate: datasource=(?[a-z-]+) depName=(?\\S+)\\s+[A-Za-z_]+: \"(?[^\"]+)\"" + "# renovate: datasource=(?[a-z-]+) depName=(?\\S+)(?: extractVersion=(?\\S+))?\\s+[A-Za-z_]+: \"(?[^\"]+)\"" ], "extractVersionTemplate": "^v?(?.*)$" } From e23b0f77bb7a4bee22790073ddb964b048981a3b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:43:02 +0000 Subject: [PATCH 5/6] fix: let lychee resolve directory links, and check the whole site Two separate faults in this job, both of which made it look like it was working. The glob was being expanded by bash before lychee ever saw it. Without globstar, ** collapses to a single directory level, so the job checked 95 links where the site has 3379. Everything below /docs/
/ -- which is almost all of the documentation -- was never looked at. Quoting the glob hands the pattern to lychee, which expands it recursively. Once it did check the whole site, four anchors reported "Cannot find fragment" against headings that are plainly present. That is the second fault: lychee does not resolve a directory link to an index file unless --index-files says so, and it is off by default. Hugo serves every page as /index.html, so lychee stopped at the directory and never opened the file the fragment lives in. --include-fragments cannot work here without it. Confirmed on a scratch tree rather than assumed: an anchor that exists passes only with --index-files, and an anchor that genuinely does not exist still fails with it. The content is fine; the check was not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/workflows/pr-checks.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index fe15381..cb8fc69 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -212,9 +212,20 @@ jobs: sudo install -m 0755 lychee-x86_64-unknown-linux-gnu/lychee /usr/local/bin/lychee lychee --version + # --index-files: without it lychee treats a link to /docs/applications/ as + # a link to a directory and stops there, so it can never look inside for + # the #fragment. Every anchor into another page then reports "Cannot find + # fragment" even though the heading is right there. Hugo serves every page + # as /index.html, so this flag is what makes --include-fragments + # usable at all here. + # + # The glob is quoted deliberately. Unquoted, bash expands it first, and + # without globstar ** collapses to a single level -- which is why this job + # was checking 95 links instead of 3379. - name: Check internal links run: | - lychee --offline --include-fragments --root-dir ./public "public/**/*.html" + lychee --offline --include-fragments --index-files index.html \ + --root-dir ./public "public/**/*.html" # ── 8. Auto-tick PR checklist ──────────────────────────────────────────────── update-checklist: From 7f9539418194f75553fec3db0a8d7fbf874fbb62 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:45:05 +0000 Subject: [PATCH 6/6] content: point the Dutch scroll-speed link at the Dutch anchor known-issues.nl.md linked to /docs/applications with the English anchor, #touchpad-scroll-speed-still-no-native-gnome-setting. relref resolves that to the Dutch page, whose heading is "Touchpad-scrollsnelheid: nog steeds geen native GNOME-instelling" and therefore carries a different slug, so the link landed at the top of the page instead of at the section. The anchor was almost certainly copied over unchanged when the page was translated. The neighbouring case, #podman--podman-desktop, survived because "Podman & Podman Desktop" is a product name and reads the same in both languages. The replacement slug is Hugo's own output for that heading, checked by rendering it rather than by deriving it by hand. It is the only language-dependent anchor in the content: the two files with anchored relrefs are known-issues and virtualization/podman, and the podman one is identical in both languages. Found by the link checker in this same branch, once it started covering more than 3% of the site. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- src/content/docs/known-issues.nl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/known-issues.nl.md b/src/content/docs/known-issues.nl.md index e911e55..1d9e19b 100644 --- a/src/content/docs/known-issues.nl.md +++ b/src/content/docs/known-issues.nl.md @@ -361,7 +361,7 @@ Scrollen met het touchpad voelde aanzienlijk sneller aan dan normaal in Brave en Een GNOME/Wayland-probleem, niet specifiek aan Brave. GNOME normaliseert scroll-events niet zoals het zou moeten, waardoor apps die niet via GTK's inputstack gaan rauwe hoge-precisie events van libinput ontvangen. Firefox en native GTK-apps werken wel goed omdat zij via GTK gaan. Veel andere apps hadden hetzelfde probleem. **Oplossing:** -[wayland-scroll-factor]({{< relref "/docs/applications#touchpad-scroll-speed-still-no-native-gnome-setting" >}}) lost dit op op GNOME-niveau door libinput-aanroepen binnen gnome-shell te onderscheppen en een scrollvermenigvuldiger toe te passen. Alles komt genormaliseerd uit. Het onderliggende GNOME-probleem staat nog steeds open upstream, maar WSF maakt het in de praktijk geen probleem meer. +[wayland-scroll-factor]({{< relref "/docs/applications#touchpad-scrollsnelheid-nog-steeds-geen-native-gnome-instelling" >}}) lost dit op op GNOME-niveau door libinput-aanroepen binnen gnome-shell te onderscheppen en een scrollvermenigvuldiger toe te passen. Alles komt genormaliseerd uit. Het onderliggende GNOME-probleem staat nog steeds open upstream, maar WSF maakt het in de praktijk geen probleem meer. **Bronnen:** - [brave-browser #36569: native touchpad scrolling op Linux Wayland](https://github.com/brave/brave-browser/issues/36569)