Problem/Motivation

Sites collect links in content that lead nowhere: deleted nodes, removed files, renamed paths, and external pages that have moved or gone away. Links that go through redirects also slow visitors down and should be updated. There's no report that finds these across all content fields.

Proposed resolution

Add a generated Broken links report that scans the same fields as Absolute URLs (link and URI fields, Custom Field link and URI subfields, and <a> elements in text fields), and lists links that:

  • lead to missing content, files, routes or paths on this site, checked without HTTP requests (entity and route URIs, files, path aliases and Redirect module redirects);
  • link to the site's 403 or 404 pages;
  • redirect, including system paths that have an alias when Redirect's Enforce clean and canonical URLs setting is on;
  • on other sites, return 404 or 410, redirect, or can't be reached. This can be turned off with Check external links.

External requests:

  • wait a second between requests to the same host, time out after 5 seconds and identify themselves with an honest User-Agent;
  • have results that are kept for a configurable time and shared by every configuration of the report;
  • list rate-limited links (429, 503) as not checked.

Generated reports should also go out of date based on their own cacheability (for example, when stored external results expire), not only the global regenerate_after setting.

Remaining tasks

  • Review the merge request.
  • Test the settings form's interval selects and Clear stored link results button. They don't have automated tests yet.
  • Follow-up: relative paths without a leading slash are resolved from /, not from the page they're on.
  • Follow-up: paths Drupal serves outside the router, such as static files outside the files directory, are reported as "Page not found".

User interface changes

  • New report at Reports » Broken links (/admin/reports/broken_links).
  • Settings form (/admin/config/system/reports):
    • The interval fields are now selects instead of seconds.
    • New Keep results of checking external links for setting.
    • New Clear stored link results button.

API changes

  • ReportStorage::isStale() uses the report's getCacheMaxAge(). It falls back to regenerate_after only for reports with neither cache tags nor a max-age.
  • New BrokenLinks::EXTERNAL_RESULTS_COLLECTION constant, for the key/value collection of external results.
  • New settings: $settings['reports_external_url_delay'], $settings['reports_external_url_timeout'] and $settings['reports_external_url_user_agent'].

Data model changes

  • New reports.settings:external_url_lifetime config key (seconds, default 86400, 0 to not keep results), with schema. There's no update hook, since the module is unreleased. The code defaults to a day when the key isn't saved.
  • New expirable key/value collection reports.external_urls.

AI disclosure

I designed the Broken links report and directed how it works: which links are checked locally versus requested, how external results are shared and expire, and how out-of-date detection uses each report's cacheability. Claude Code (Claude Opus) wrote most of the code, the tests and the README changes from those decisions. I reviewed the changes and ran the kernel tests, phpcs and phpstan locally.

Comments

dave reid created an issue.