A small local pipeline that pulls a year of your Strava activities (and their photos), then renders two static outputs:
report.html- detailed tables and charts of your training volume.infographic.html- an editorial "How I trained" poster (1080px wide), which you can screenshot to a PNG.
Everything runs locally against your own Strava account. The only file you edit
to make it yours is config.py.
- Python 3 and
pip. - A Strava account (and a free Strava API application - set up below).
- Optional: a coding agent (Claude Code, Codex, etc.) - only if you want the poster to break down your strength work by reading workout-tracker screenshots. See Optional: workout-photo vision.
- Optional: Google Chrome + Pillow (
pip install pillow) - only to render the poster to a PNG. The HTML renders fine in any browser without them.
- Go to https://www.strava.com/settings/api.
- Create an application. For Authorization Callback Domain enter
localhost(this matches the local OAuth flow inauth_setup.py). - Copy your Client ID and Client Secret - you'll paste them in the next step.
pip install -r requirements.txtpython auth_setup.pyThis prompts for your Client ID and Secret, opens a browser to authorize the
activity:read_all scope, and writes a .env file with your credentials and a
refresh token. .env is gitignored - never commit it.
Edit config.py:
START_DATE- the start of your training window (change the year).HEADLINE,SUBTITLE,TAGLINE,PAGE_TITLE- the poster's editorial copy.RACES- your race milestones (Strava's activity feed has no description field, so named races are listed by hand). Set this to[]if you have none.EXCLUDED_TITLE_KEYWORDS- workout titles to leave out of strength/mobility counts.
python fetch_strava.py # activities + photos -> cache/ (idempotent)
# (optional) vision step - see below
python build_report.py # -> report.html
python build_infographic.py # -> infographic.htmlfetch_strava.py writes everything into cache/ and is resumable - re-running
it skips already-downloaded data.
If your Strava activities include workout-tracker screenshots, you can have an
agent read them so the poster's "What Strength Covered" section is populated.
After fetch_strava.py runs, it prints any photos lacking a sidecar. For each
photo in cache/photos/, your agent reads the image and writes a JSON sidecar to
cache/vision/<activity_id>_<index>.json with this shape:
{
"type": "workout",
"exercises": [
{"name": "Goblet squat", "sets": 4, "reps": "8-10", "weight": "24kg",
"muscle_groups": ["legs"]}
],
"notes": "optional"
}- Use
"type": "other"with an emptyexerciseslist for non-workout photos (e.g. scenery). Always write a sidecar so the photo isn't re-listed. muscle_groupsdrive strength-vs-mobility classification: usepush/pull/legsfor strength logs andmobilityfor mobility/stability lists.
This step is entirely optional. With no sidecars, the infographic still
renders - the "What Strength Covered" section is just empty, and strength
sessions tagged in Strava as WeightTraining are still counted.
infographic.html is self-contained (fonts are embedded), so you can open it in
a browser and screenshot it. To do it from the command line with headless
Chrome (and pip install pillow to crop the trailing margin):
google-chrome --headless --disable-gpu --no-sandbox --hide-scrollbars \
--force-device-scale-factor=2 --screenshot=_raw.png --window-size=1080,3200 \
"file://$PWD/infographic.html"
# then crop _raw.png down to the poster (background #cfc9ba) with Pillow- The poster surfaces these activity types: running (Run/TrailRun), strength,
mobility, swimming, yoga, and walks/hikes. Other sports (e.g. Pickleball) are
counted in
report.htmlbut don't get their own row on the poster. - Strength-vs-mobility classification is title-first and keys off the
muscle_groupsvocabulary above - keep it consistent when writing sidecars. - This assumes you have at least some activities in the configured window.
- There are no tests or build tooling; the scripts read/write
cache/and are idempotent.CLAUDE.mdhas additional notes aimed at coding agents.