{"id":2865,"date":"2026-11-08T23:18:12","date_gmt":"2026-11-08T23:18:12","guid":{"rendered":"https:\/\/hauweele.net\/~gawen\/blog\/?p=2865"},"modified":"2026-11-08T23:18:59","modified_gmt":"2026-11-08T23:18:59","slug":"clubmirror-v0-1-2-a-static-website-generator-for-strava-club-events","status":"publish","type":"post","link":"https:\/\/hauweele.net\/~gawen\/blog\/?p=2865","title":{"rendered":"clubmirror v0.1.2: a static website generator for Strava club events"},"content":{"rendered":"<p>My <a href=\"https:\/\/www.bekane.cc\">local bike shop<\/a> posts its group rides in a Strava club, but you need a Strava account to see them. So I wrote a small tool that publishes those events as a static page that they could link. It sat in a private repository for far too long because I kept thinking &#8220;it&#8217;s not ready yet&#8221;. But it&#8217;s public now, as <a href=\"https:\/\/github.com\/gawen947\/clubmirror\"><strong>clubmirror<\/strong><\/a>.<\/p>\n<p>Actually it&#8217;s part of a process where I try to publish projects that were sitting in private repositories for far too long because I thought that they were not ready yet. This is the first one, but you can expect more of them to come.\n<\/p>\n<h2>What it does<\/h2>\n<p><a href=\"https:\/\/github.com\/gawen947\/clubmirror\"><strong>clubmirror<\/strong><\/a> is a Rust command-line tool meant to run from cron\/periodic. Each run it:<\/p>\n<ul>\n<li>fetches the club&#8217;s group events from the Strava API,<\/li>\n<li>renders them with <a href=\"https:\/\/docs.rs\/tera\">Tera<\/a> templates into plain static files,<\/li>\n<li>writes an <code>.ics<\/code> calendar people can subscribe to,<\/li>\n<li>downloads the GPX\/TCX files of the routes attached to the events.<\/li>\n<\/ul>\n<p>Then it exits, and that&#8217;s it. So there is no daemon, no database, no server-side code. It handles any number of clubs, each with its own templates, timezone and time window.<\/p>\n<h2>Try it without a Strava account<\/h2>\n<p>The repository includes a sample of fake events to play with:<\/p>\n<pre><code>git clone https:\/\/github.com\/gawen947\/clubmirror\r\ncd clubmirror\r\nchmod 600 examples\/config.toml\r\ncargo run -- -c examples\/config.toml -v\r\npython3 -m http.server -d public 8000   # open http:\/\/localhost:8000<\/code><\/pre>\n<p>Edit <code>templates\/default\/<\/code>, run again, and see the result. For a real club you need a Strava API application and a refresh token (cf README).<\/p>\n<p>clubmirror is unofficial and not affiliated with Strava. Strava&#8217;s API terms restrict how its data can be displayed, so check them for your use.<\/p>\n<h2>What&#8217;s next?<\/h2>\n<ul>\n<li>Scripted per-club quirks with <a href=\"https:\/\/rhai.rs\">Rhai<\/a>, instead of hardcoded ones<\/li>\n<li>Proper debug options<\/li>\n<li>Event filtering, for example keeping members-only events out of the calendar<\/li>\n<li>Configurable event duration in the calendar<\/li>\n<li>Processing clubs in parallel<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>My local bike shop posts its group rides in a Strava club, but you need a Strava account to see them. So I wrote a small tool that publishes those events as a static page that they could link. It &hellip; <a href=\"https:\/\/hauweele.net\/~gawen\/blog\/?p=2865\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_hide_featured_image":false,"_jetpack_featured_image_duplicate_dismissed":0,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1194],"tags":[1242,1243,1171,1236,1176,1237,1238,1239,1240,1241],"class_list":["post-2865","post","type-post","status-publish","format-standard","hentry","category-software","tag-cycling","tag-running","tag-rust","tag-rust-cli","tag-self-hosted","tag-sports","tag-static-site","tag-static-site-generator","tag-strava","tag-strava-api"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=\/wp\/v2\/posts\/2865","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2865"}],"version-history":[{"count":0,"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=\/wp\/v2\/posts\/2865\/revisions"}],"wp:attachment":[{"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2865"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2865"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hauweele.net\/~gawen\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2865"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}