Skip to content

add strict check - to catch missing images / bad links before publish #15

Description

@dr-matt-smith

Fail the build on root-absolute URLs, so the Pages mirror matches the custom domain

Summary

docs/index.md linked its screenshot as /images/python_markdown.webp. That image
rendered on https://learn.celbridge.org/ and 404'd on
https://celbridge-org.github.io/celbridge-docs/, even though both hosts serve the
same build from the deploy branch.

The content bug is already fixed (b826460). This issue is about the missing guard:
nothing in the build or the publish workflow catches a root-absolute URL, so the same
mistake ships again the next time someone writes one.

Why one build broke on one host and not the other

A leading / resolves against the host root, not the site root:

Written in Markdown learn.celbridge.org celbridge-org.github.io
/images/x.webp learn.celbridge.org/images/x.webp — correct celbridge-org.github.io/images/x.webp404, the /celbridge-docs/ prefix is lost
images/x.webp correct correct

The custom domain serves the site at the root, so an absolute path happens to work
there. GitHub Pages serves the same build under /celbridge-docs/, so the same path
points outside the site entirely. Page-relative URLs resolve correctly under both.

This is already documented in the README under Editing → Images; it is a rule with
no enforcement behind it.

Why --strict did not catch it

zensical build --strict validates internal links between pages. It does not
check asset URLs, and an absolute URL is perfectly valid markup — it just points
somewhere else. The build passed, the publish workflow passed, and the mirror shipped
with a broken image.

Proposed fix: a post-build check

Add scripts/check_absolute_paths.py, and call it from build.py after the build, in
the same place the redirect stubs are written. CI picks it up for free, because the
workflow already runs python build.py.

This mirrors how the celbridge-website repo runs its check_front_matter_links.py and
check_no_third_party.py after its build.

scripts/check_absolute_paths.py

"""Fail the build on a root-absolute URL in the built site.
 
The site is published at two base paths: learn.celbridge.org serves it at the
root, and GitHub Pages serves the same build under /celbridge-docs/. A URL
written as /images/x.webp resolves against the host, so it points outside the
site on Pages and 404s there while working on the custom domain. Page-relative
URLs resolve correctly under both.
 
404.html is exempt: a 404 is served in place of any URL at any depth, so its
links cannot be relative. It is built from site_url and only resolves on the
custom domain, which is a known limit of publishing one build to two base paths.
"""
 
import re
import sys
from pathlib import Path
 
REPO_ROOT = Path(__file__).resolve().parent.parent
SITE_FOLDER = REPO_ROOT / "site"
 
EXEMPT_FILES = {"404.html"}
 
# A src or href whose value starts with a single / - not // , which is a
# protocol-relative URL naming another host.
ABSOLUTE_URL = re.compile(r'(?:src|href)="(/(?!/)[^"]*)"')
 
 
def main() -> int:
    if not SITE_FOLDER.is_dir():
        print(f"error: {SITE_FOLDER} does not exist - run the build first")
        return 1
 
    findings = []
 
    for page in sorted(SITE_FOLDER.rglob("*.html")):
        if page.name in EXEMPT_FILES and page.parent == SITE_FOLDER:
            continue
 
        for url in ABSOLUTE_URL.findall(page.read_text(encoding="utf-8")):
            findings.append((page.relative_to(SITE_FOLDER), url))
 
    if findings:
        print(f"{len(findings)} root-absolute URL(s) in the built site:")
        for page, url in findings:
            print(f"  {page}: {url}")
        print()
        print("These break on GitHub Pages, which serves the site under /celbridge-docs/.")
        print("Write them page-relative instead, e.g. images/x.webp or ../images/x.webp.")
        return 1
 
    print(f"No root-absolute URLs in {SITE_FOLDER.name}/")
 
    return 0
 
 
if __name__ == "__main__":
    sys.exit(main())

build.py

 def build() -> int:
     # --strict fails the build on a broken internal link, rather than
     # publishing a site with holes in it.
     print("==> building (strict)")
     run_step(*ZENSICAL, "build", "--clean", "--strict")
 
     redirects()
 
+    # A root-absolute URL works on the custom domain and 404s on the Pages
+    # mirror, which serves the same build under a subpath.
+    print("==> checking for root-absolute URLs")
+    run_step(PY, "scripts/check_absolute_paths.py")
+
     print()
     print("Done. Output in site/")
     return 0

The check runs after the redirect stubs so that it sees the stubs too — they are
written into site/ and carry URLs of their own.

Verified

Run against the current build, both ways:

  • with the bug reintroduced in docs/index.md, the check fails with exit 1 and prints
    index.html: /images/python_markdown.webp
  • with the fix in place, it passes: No root-absolute URLs in site/

Known limitation this does not solve: 404.html

site/404.html is built with root-absolute URLs by design — a 404 is served in place
of any URL at any depth, so its links cannot be relative — and they are built from
site_url, i.e. learn.celbridge.org. On the Pages mirror, the 404 page's nav links
point at celbridge-org.github.io/01_about/ and its stylesheet 404s, so a missing page
there renders unstyled with dead links.

No build-time check fixes this; one build genuinely cannot serve two base paths for
that one page. The options are to accept it (it affects only the mirror's 404 page), to
drop the Pages mirror and treat learn.celbridge.org as canonical, or to build twice
with a different site_url per host — which gives up "one build feeds both". The check
above exempts 404.html so this known case does not fail every build.

Acceptance criteria

  • scripts/check_absolute_paths.py exists and is called from build.py
  • python build.py fails when any page other than 404.html carries a
    root-absolute src/href, naming the file and the URL
  • the publish workflow fails the same way, with no workflow edit needed
  • the README's Editing → Images rule points at the check as its enforcement

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions