A site is a collection of documents connected by references. This capstone supplies that collection as a Python mapping from absolute URL strings to HTML strings. The URLs are identifiers for local fixtures; no file, DNS lookup or network request is needed. A known page is present in the mapping. A broken internal target is absent from that mapping. An external target belongs to another syntactic origin and has unknown availability.
Validate the collection before reporting it. Convert the entry and every supplied page key with document_key. Require the entry to be present, all pages to share its canonical origin, and every normalized page key to be distinct. Reject collisions rather than silently letting dictionary order select one source. Two keys differing only by a default port or fragment can collide under the declared policy. Different queries remain separate pages.
The supplied page scanner gathers anchor hrefs and the first base element with a string href, including an empty string. Fixtures have a static, well-formed base declaration in the head before links, unique attributes and no dynamic DOM behavior. A later base must not replace the first. Resolve a relative declared base against the page’s canonical document URL, then resolve its hrefs against that base. The scanner includes anchor start events even without a closing tag because destination analysis needs no label.
from urllib.parse import urljoin
page = "https://example.test/docs/start.html"
base = urljoin(page, "../assets/")
print(urljoin(base, "guide.html#install"))
print(urljoin(base, "/contact"))For each accepted target, classify its origin. Known internal targets become graph edges. Missing internal targets become broken rows. External targets become external rows. Rejected or unresolvable extracted references increase an ignored counter. A declared base that makes urljoin raise is a configuration error and propagates ValueError. Count repeated broken or external occurrences separately for each (source, target) pair. A set is enough for graph adjacency because multiple identical edges do not change reachability, but it would lose report occurrence counts.
Inspect all supplied pages, including unreachable ones. An orphan page can still contain a broken reference worth reporting. Then traverse known internal edges from the entry using a work list and a seen set. Add discovered neighbors until the work list is empty. A self-link or cycle must terminate, and a three-hop chain must reach its last page. Missing and external targets never enter this traversal.
graph = {"home": {"guide"}, "guide": {"example"}, "example": {"home"}, "orphan": set()}
seen, pending = set(), ["home"]
while pending:
current = pending.pop()
if current in seen:
continue
seen.add(current)
pending.extend(graph[current] - seen)
print(sorted(seen))
print(sorted(set(graph) - seen))Return sorted reachable and unreachable page keys, sorted broken/external rows and the ignored count. audit_json applies the earlier standalone JSON formatting policy. Preserve input mappings and strings; output ordering must not depend on input insertion order or set traversal order.
Build failure cases as carefully as the happy path: empty maps, absent entries, alias collisions, cross-origin page keys, first-base precedence, query variants, cycles and repeated missing targets. The report is an audit of the supplied snapshot. It does not prove a server returns 200, a fragment exists, a browser can render the page, or a request destination is authorized. Live crawling adds HTTP preparation, bounded work, robots conventions, remote failure handling and request-security decisions beyond this free local course.