Hands-On: Follow a Project Through the Catalog
I can describe the project catalog all day, but I find it easier to understand once I can see what a single refresh writes. This lab runs a frozen copy of the current project generator against three invented repositories. It keeps the experiment out of the real site and replaces GitHub requests and image rendering with local stand-ins.
The Part 5 article follows the whole publishing path. Here I am testing a smaller claim: project order survives generation, my overrides win where I supply them, visibility changes the generated fields, and a new project gets starter files. The menu and page still need a Jekyll build; this Python run does not render them.
Run the Invented Catalog
Download the project catalog lab and its SHA-256 checksum. The runner uses Python 3 plus the generator’s existing requests, PyYAML, and beautifulsoup4 dependencies. It writes only into a temporary directory and removes that directory when it finishes.
You can inspect the instructions and runner first:
README.md markdown View source
# Project catalog lab
This exercise runs a frozen copy of the site's `utils/bin/fetch_og.py` with invented repositories in a temporary directory. The runner replaces GitHub fetching and image rendering with local stubs. It does not read or write the real site's catalog or pages, and it makes no network request.
The copy is here to show the current generator's data merge, visibility, ordering, and first-page scaffolding. It is a teaching snapshot, not an installable project-publishing package.
Run with Python 3 and the generator's existing Python dependencies (`requests`, `PyYAML`, and `beautifulsoup4`):
```bash
python3 run_lab.py
python3 run_lab.py --reorder
python3 run_lab.py --show-hidden
```
Each run prints the generated project entries in catalog order, whether each one has a repository link, the final ExampleTool description, and the starter files. `--reorder` moves QuietTool to the first generated position. `--show-hidden` changes HiddenTool from `none` to `public`, adding a generated repository URL. These are changes to the invented inputs; they do not render Jekyll's menus. Each run ends with `PASS: ordered projects, visibility, overrides, and new-project scaffolding`. All generated files are discarded when the temporary directory closes.
run_lab.py python View source
#!/usr/bin/env python3
"""Exercise the site's frozen project generator without site data or network."""
import importlib.util
import sys
import tempfile
from argparse import ArgumentParser, Namespace
from pathlib import Path
from unittest import mock
import yaml
ROOT = Path(__file__).resolve().parent
sys.dont_write_bytecode = True
SPEC = importlib.util.spec_from_file_location("fetch_og_lab", ROOT / "source/fetch_og.py")
fetch_og = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(fetch_og)
def main():
parser = ArgumentParser(description="Run the invented project catalog through the site's generator")
parser.add_argument("--reorder", action="store_true", help="move QuietTool ahead of ExampleTool in the authored catalog")
parser.add_argument("--show-hidden", action="store_true", help="change HiddenTool's authored visibility from none to public")
args = parser.parse_args()
repositories = [
{"owner": "example", "name": "ExampleTool", "overrides": {"title": "Example Tool", "description": "An invented tool for the lab.", "visibility": "public"}},
{"owner": "example", "name": "QuietTool", "overrides": {"title": "Quiet Tool", "description": "A private example.", "visibility": "private"}},
{"owner": "example", "name": "HiddenTool", "overrides": {"title": "Hidden Tool", "visibility": "none"}},
]
if args.reorder:
repositories[0], repositories[1] = repositories[1], repositories[0]
if args.show_hidden:
repositories[2]["overrides"]["visibility"] = "public"
metadata = {"ExampleTool": {"description": "A fetched description.", "visibility": "public", "language": "Python"}, "QuietTool": None, "HiddenTool": None}
with tempfile.TemporaryDirectory(prefix="project-catalog-lab-") as scratch:
base = Path(scratch)
(base / "html/_data").mkdir(parents=True)
with mock.patch.object(fetch_og, "setup_environment", return_value=base), \
mock.patch.object(fetch_og, "load_repository_config", return_value=repositories), \
mock.patch.object(fetch_og, "fetch_github_data", side_effect=lambda owner, name: metadata[name]), \
mock.patch.object(fetch_og, "generate_project_card", return_value="/assets/images/projects/ExampleTool.png"), \
mock.patch.object(fetch_og, "cache_image", side_effect=lambda url, name, base_dir: url), \
mock.patch.object(fetch_og, "parse_args", return_value=Namespace(refresh_images=False)), \
mock.patch.object(fetch_og.requests, "get", side_effect=AssertionError("network request attempted")):
fetch_og.main()
output = yaml.safe_load((base / "html/_data/github_projects.yml").read_text(encoding="utf-8"))["projects"]
expected_order = ["QuietTool", "ExampleTool", "HiddenTool"] if args.reorder else ["ExampleTool", "QuietTool", "HiddenTool"]
assert [p["name"] for p in output] == expected_order
projects = {p["name"]: p for p in output}
assert projects["ExampleTool"]["title"] == "Example Tool"
assert projects["ExampleTool"]["description"] == "An invented tool for the lab."
assert projects["ExampleTool"]["repo_url"] == "https://github.com/example/ExampleTool"
assert projects["QuietTool"]["visibility"] == "private" and "repo_url" not in projects["QuietTool"]
assert projects["HiddenTool"]["visibility"] == ("public" if args.show_hidden else "none")
assert ("repo_url" in projects["HiddenTool"]) == args.show_hidden
assert (base / "html/projects/ExampleTool.md").exists()
assert (base / "html/projects/ExampleTool/_drafts/template-blog-entry.md").exists()
intro = list((base / "html/projects/ExampleTool/_posts").glob("*.md"))
assert len(intro) == 1 and "draft: true" in intro[0].read_text(encoding="utf-8")
print("Generated data, in catalog order:")
for project in output:
print(f" {project['name']}: {project['visibility']}; repo link: {'yes' if 'repo_url' in project else 'no'}")
print(f"ExampleTool description: {projects['ExampleTool']['description']}")
print("New ExampleTool files: landing page, _drafts/template-blog-entry.md, draft-marked _posts introduction")
print("PASS: ordered projects, visibility, overrides, and new-project scaffolding")
if __name__ == "__main__":
main()
With the ZIP and checksum in one directory:
shasum -a 256 -c project-catalog-lab.zip.sha256
unzip project-catalog-lab.zip
cd project-catalog-lab
python3 run_lab.py
python3 run_lab.py --reorder
python3 run_lab.py --show-hidden
In the first run, Example Tool has both a fetched description and a manual description. Look for the manual one in the printed result. Quiet Tool has no fetched metadata and is marked private; it gets a page but no repository URL in its generated entry. Hidden Tool is marked none; it remains in generated data so the site’s Liquid views can filter it.
Now compare the two variations. --reorder moves Quiet Tool ahead of Example Tool in the invented input, and the printed generated order changes with it. --show-hidden changes Hidden Tool’s visibility to public; its generated entry gains a repository URL. The runner checks each result before printing PASS. The commands do not edit this site’s YAML or render its menus, but they let you change the same inputs those later views consume.
This run exercises the project-generator branch. The reader-facing menus are the next build's job.
Read the Result Carefully
The runner also checks for Example Tool’s landing page, draft template, and draft-marked starter introduction. Those files show what the generator starts. They do not make the introduction ready to publish. I would still review the words, image, route, tags, and any private detail before treating it as an actual project announcement.
The lab stubs the network and card renderer on purpose, so it does not prove GitHub availability, image rendering, or the final HTML. It does execute the current generator’s project-entry and scaffolding code. If its assertions pass, they support those particular behaviors on the frozen snapshot included in the archive.
Next Work
The generated card and page give a project a place to start. Part 6 looks at how diagrams and source files inside an article become useful editorial material rather than a pile of attachments.
Join the Discussion
Comments for this post live in GitHub Discussions. That keeps moderation in one place and gives the conversation a stable home.