Skip to content

a11y: image alt text — duplicate/thin alt in README + docs, and the generated tutorial ships identical generic alt for every repo #197

Description

@alphacrack

User story

As a screen-reader user reading readme2demo's README — or reading a tutorial.md that readme2demo generated for someone else's project — I want every image to carry alt text that says what that specific image shows, so that I get the same information sighted readers do instead of hearing "Contributors, Contributors" or the same generic sentence on every tutorial ever generated.

What

Two halves, one PR.

(a) Hand-written docs — fix thin and duplicated alt text.

Where Current alt Problem
README.md:3 tests Badge state is invisible; announces as a bare word. Suggest CI tests status.
README.md:185 Contributors It's a shields.io badge rendering a count, not the people. Suggest Contributor count.
README.md:188 Contributors Identical alt to the image three lines above it. Two adjacent images, same announcement, no way to tell them apart. Suggest Contributor avatars.
examples/README.md:32 readme2demo self-demo Restates the filename; says nothing about what plays.
examples/README.md:36 toolhive demo Same, and "demo" is filler next to a link already labelled demo.gif.

Already fine, leave alone: README.md:4 (License: MIT), README.md:5 (Python 3.10+), README.md:12 and docs/index.md:12 (both readme2demo running on its own repo — verified demo — genuinely descriptive).

(b) Generated artifacts — the product bug.

src/readme2demo/templates/tutorial.md.j2:46 hardcodes one static string:

![Demo video: every step above executing in a clean container](demo.gif)

It is not empty, but it is identical in every tutorial readme2demo has ever produced, for every repo. examples/toolhive/tutorial.md:121 is byte-for-byte that string — in a tutorial that is entirely about ToolHive.

That's out of step with the rest of the file. Directly above it, the SEO/GEO shape CLAUDE.md tells us to preserve is repo-specific by design: seo_title() (tutorial.py:165-169) renders How to install and run stacklok/toolhive — verified tutorial, and seo_description() (tutorial.py:172-180) derives from the run's own intro. Alt text is the one piece of that shape still generic — and alt text is indexed content, so this is an SEO regression as well as an a11y one.

Fix: interpolate a variable already passed to the template. title is passed at tutorial.py:207; repo_url at tutorial.py:214. Something like:

![Demo video: {{ title }} — every step above executing in a clean container](demo.gif)

Pick whichever of title / _repo_name(repo_url) reads better and degrades sanely when repo_url is empty (guide-only runs pass repo_url=""seo_title already handles that by falling back).

Explicitly out of scope (do not grow the PR):

  • Embedding the GIF in step_by_step.md. It currently embeds no image at alltutorial.py:625-631 only appends the prose line Watch every step execute in demo.mp4 / demo.gif alongside this file. There is no alt-text bug there; adding an image is a separate product decision.
  • Adding video / VideoObject to howto.jsonld (tutorial.py:248-270 has no video key). Worth a follow-up issue, not this one.
  • WCAG 2.2.2 (Pause/Stop/Hide) for the auto-looping hero GIF at README.md:12. Real concern, much bigger fix (poster frame + click-to-play), file separately.
  • Any change to distill.py, normalize.py, engines/, or commands.sh / demo.tape generation.

Why

New category for this board: accessibility. Nothing under the 93 open issues covers it (see search results in the notes), so this is the first one — hence the proposed accessibility label.

Half (b) is a real user-facing defect, not polish: readme2demo's entire pitch is that it publishes good docs on your behalf. Every tutorial it has generated carries the same context-free image description. The tool that fixes other people's docs should not be the one shipping the generic alt text.

Half (a) is a genuine incident-shaped bug in the small: README.md:185 and README.md:188 are two different images, three lines apart, with byte-identical alt text — a screen reader announces "Contributors" twice with nothing distinguishing them. That pair arrived with the contributors section touched in #180.

Pointers

Verified by reading each file at the commit on main:

  • README.md:3-5 — three badges: alt tests, License: MIT, Python 3.10+.
  • README.md:12 — hero, alt readme2demo running on its own repo — verified demo. Good already.
  • README.md:185[![Contributors](https://img.shields.io/github/contributors/alphacrack/readme2demo?style=flat-square)](...)
  • README.md:188<img src="https://contrib.rocks/image?repo=alphacrack/readme2demo" alt="Contributors" /> — same alt as :185.
  • docs/index.md:12 — alt readme2demo running on its own repo — verified demo. Good already.
  • examples/README.md:32,36 — alt readme2demo self-demo / toolhive demo.
  • src/readme2demo/templates/tutorial.md.j2:43-49 — the {% if has_video %} Demo block containing the hardcoded alt string.
  • src/readme2demo/tutorial.py:205has_video = (run_dir / "demo.gif").exists() or (run_dir / "demo.mp4").exists() — a filesystem check, not LLM output.
  • src/readme2demo/tutorial.py:206-228template.render(...); title and repo_url are already in scope.
  • src/readme2demo/tutorial.py:165-180seo_title / seo_description, the repo-specific SEO shape the alt text should match.
  • examples/toolhive/tutorial.md:121 — the committed proof that the generic string ships as-is.
  • tests/test_tutorial.py:409 test_tutorial_md_front_matter_and_provenance — renders the full template and asserts the front matter and provenance footer, but never creates demo.gif/demo.mp4 in tmp_path, so has_video is False and the Demo block is currently not covered by any test.

Grounding implication: none. Alt text is prose/markup. This changes no command that reaches tutorial.md, step_by_step.md, commands.sh, or demo.tape; it touches only the {% if has_video %} block, gated by a filesystem check rather than model output, and leaves enforce_commands untouched. Calling it out so a reviewer doesn't have to re-derive it.

Acceptance criteria

  • README.md:3 alt describes the badge as a CI status, not the bare word tests.
  • README.md:185 and README.md:188 carry distinct, descriptive alt text (count badge vs. avatar grid).
  • examples/README.md:32,36 alt text describes what the demo shows, not the filename.
  • templates/tutorial.md.j2:46 interpolates a per-run value so two tutorials for different repos produce different alt text.
  • Guide-only runs (repo_url="") still render sensible alt text — no empty segment, no stray dash.
  • A new test in tests/test_tutorial.py creates demo.gif in tmp_path before calling run_tutorial, then asserts the rendered tutorial.md alt text contains the repo/tutorial identifier. Carry a """Regression: ...""" docstring per the house convention.
  • examples/toolhive/tutorial.md is left as-is (committed run output — regenerating it is not part of this PR).
  • python -m pytest tests/ -q green; ruff check src/ tests/ clean.

Notes for contributors

Honest prerequisites: pure Python + Markdown + one Jinja line. No Docker, no API key, no network. You will not run the pipeline.

pip install -e ".[dev]"
python -m pytest tests/ -q        # ~1.5s, no docker/network/API
ruff check src/ tests/

The only slightly fiddly part is the test: test_tutorial_md_front_matter_and_provenance at tests/test_tutorial.py:409 is your template, but note it does not create demo.gif, so the block you're changing never renders there. Your new test needs (tmp_path / "demo.gif").write_bytes(b"") (or .touch()) before run_tutorial(...) to flip has_video to True. Everything else in that test — the monkeypatch.setattr(llm_mod, "complete_json", ...) stub that avoids a real LLM call — you can copy verbatim.

Please don't regenerate anything under examples/ — those are committed run outputs kept as proof, and hand-editing them breaks the provenance story.

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityAccessibility of docs and generated artifactsarea:docsdocs/ site contentarea:tutorialtutorial.py — tutorial/step-by-step/troubleshooting/jsonld artifactsdocumentationDocs, README, docs-sitegood first issueSmall, self-contained, newcomer-friendly

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions