Broken Links¶
Console script that lists internal links pointing to content which no longer exists or has been moved to the trash. Editors keep links to pages that are later renamed, deleted or trashed — the stored link stays behind and only fails when a visitor clicks it. The script walks all content of every Plone site in the instance and reports those links.
Key characteristics:
Operates on all Plone sites within the Zope instance
Reads only — no content is modified, nothing is committed
Checks rich text, link widget fields and JSON schema link fields
Reports missing and trashed targets
Logs a per-site summary
Usage¶
./bin/broken-links <path-to-zope.conf> [--host HOSTNAME ...]
Arguments:
Argument |
Description |
|---|---|
|
Absolute path to the Zope configuration file ( |
|
Hostname whose absolute URLs are treated as internal. Repeatable. |
Links written as absolute URLs (https://www.example.ch/de/page) are only
checked when their host is known. Outside of a running server the instance
does not know its public hostnames, so pass every one of them with --host —
absolute URLs on any other host count as external and are skipped.
Examples:
# All sites of the instance
./bin/broken-links instance/etc/zope.conf
# Also check absolute URLs written against the live site
./bin/broken-links instance/etc/zope.conf --host www.example.ch --host example.ch
In a container the config file depends on the storage — relstorage.conf,
zeo.conf or zope.conf in /app/etc/:
docker compose exec -u plone backend /app/bin/broken-links /app/etc/relstorage.conf
Output¶
Findings are grouped per object. The first line is the path and portal type of the object holding the link, followed by one line per broken link with the field name, the stored link and the reason:
/Plone/gerichte/rechtsprechung/uebersicht (Block)
text: resolveuid/6f6d6a0834914414bce433f0a962689f -> target does not exist
text: /themen/wohnen/schlichtungsstelle -> target does not exist
/Plone/verwaltung/kontakt (ContentPage)
cta_href: resolveuid/1d8d8958263942d297f520d5cc18252c -> target is in the trash
Plone: 3 broken internal link(s).
othersite: 0 broken internal link(s).
Redirect to a file for large sites — a grown site easily produces thousands of lines:
./bin/broken-links instance/etc/zope.conf > broken-links.txt
Reasons¶
Reason |
Meaning |
|---|---|
|
The UID is unknown or the path cannot be traversed. The link is dead. |
|
The target was deleted by an editor and sits in the trash container. |
Trashed content is worth its own reason: it stays in the catalog, so the link still resolves internally, but the public site answers with a 404. Such links are fixed either by restoring the target or by repointing the link.
What is checked¶
Field type |
Where links come from |
|---|---|
Rich text |
|
Text line with the link widget |
The stored value, e.g. a call to action link |
JSON field |
Every value whose JSON schema marks it with |
How links are resolved¶
resolveuid/<uid>is looked up by UID. Everything after the UID is a view on the target (/@@images/image/preview) and is not checked separately.${portal_url}and${navigation_root_url}are replaced by the path they stand for, relative to the object holding the link.Absolute paths are traversed from the Zope root first (
/Plone/de/page) and then from the portal, so public frontend paths without the portal id (/de/page) resolve as well.Relative paths are traversed from the container of the object. A link stored in a block is resolved against the container of its page, because that is the URL the block is rendered at.
The view part of a path (everything from
/@@or/++) is cut off before traversal. Whether/@@download/file/report.pdfrenders says nothing about the existence of the linked content.
Skipped, because they are not internal links: other URL schemes
(mailto:, tel:, …), absolute URLs on unknown hosts, and links that carry
only a fragment or a query string.
Not checked¶
External links — no HTTP request is made, the script never leaves the ZODB.
Relations — reference fields (
RelationValue) are not link fields and are not inspected.Content in the trash — trashed content is skipped as a source of links; fixing links inside deleted content has no value.
Scheduling with Cron¶
The script is safe to run unattended, for example as a weekly report:
# Every Monday at 3:00 AM
0 3 * * 1 /path/to/bin/broken-links /path/to/instance/etc/zope.conf > /var/log/broken-links.txt
Storage note¶
The script opens the database like any other Zope process. With RelStorage
or ZEO it runs next to the live instance without interfering. With a direct
FileStorage only one process can hold the storage lock, so the instance has
to be stopped, or the script has to be pointed at a copy of Data.fs.
Logging¶
The script logs to the wcs.backend.linkcheck logger at INFO level.
Output is written to stdout with the format %(message)s.