From 69d35b24621aa2117d9cec90a58a66085b39f27f Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Wed, 21 Oct 2026 08:54:23 +0800 Subject: [PATCH] feat: tag sponsor links with UTM parameters Sponsor links carried no UTM tags, so sponsors saw awesome-python clicks at best as generic referral traffic and could not tell README clicks from website clicks. README sponsor entries now carry utm_source=awesome-python&utm_medium=sponsorship&utm_campaign=sponsor&utm_content=readme, and the website build rewrites utm_content to website for the sponsor band. New sponsor entries in README.md need the same UTM suffix. Co-Authored-By: Claude --- README.md | 2 +- SPONSORSHIP.md | 2 ++ website/build.py | 13 ++++++++++ website/tests/test_build.py | 49 +++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index abf49fef..83107e9f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An opinionated guide to the best Python frameworks, libraries, and tools. ## **Sponsors** -- [iPulse AI](https://ipulseai.com) - Open Agentic Investment Research Platform. Inspect deep multi-agent market forecasts and stock picks. +- [iPulse AI](https://ipulseai.com?utm_source=awesome-python&utm_medium=sponsorship&utm_campaign=sponsor&utm_content=readme) - Open Agentic Investment Research Platform. Inspect deep multi-agent market forecasts and stock picks. > **Sponsor slots open**, from $99/month. The **#10 most-starred repo on GitHub**. Put your product in front of Python developers. [Become a sponsor](https://awesome-python.com/sponsorship/). diff --git a/SPONSORSHIP.md b/SPONSORSHIP.md index 6e4e9846..8e20d6cb 100644 --- a/SPONSORSHIP.md +++ b/SPONSORSHIP.md @@ -50,6 +50,8 @@ Email [sponsorship@awesome-python.com](mailto:sponsorship@awesome-python.com?sub One upfront payment per term. Setup takes less than 24 hours. +Sponsor links carry UTM tags, so clicks show up in your own analytics as `awesome-python / sponsorship`. + ## Previously Sponsored By - [Warp](https://www.warp.dev/) - The terminal for modern developers. diff --git a/website/build.py b/website/build.py index fde14149..5d90459b 100755 --- a/website/build.py +++ b/website/build.py @@ -11,6 +11,7 @@ from collections.abc import Sequence from datetime import UTC, datetime from pathlib import Path from typing import TypedDict +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from jinja2 import Environment, FileSystemLoader from markdown_it import MarkdownIt @@ -130,6 +131,16 @@ def extract_github_repo(url: str) -> str | None: return m.group(1) if m else None +def sponsor_website_url(url: str) -> str: + """Set utm_content=website so sponsors can tell website clicks from README clicks.""" + parts = urlsplit(url) + pairs = parse_qsl(parts.query, keep_blank_values=True) + if not any(k == "utm_content" for k, _ in pairs): + return url + query = [(k, "website" if k == "utm_content" else v) for k, v in pairs] + return urlunsplit(parts._replace(query=urlencode(query))) + + def load_stars(path: Path) -> dict[str, dict]: """Load star data from JSON. Returns empty dict if file doesn't exist or is corrupt.""" if path.exists(): @@ -749,6 +760,8 @@ def build(repo_root: Path) -> None: parsed_groups = parse_readme(readme_text) sponsors = parse_sponsors(readme_text) + for sponsor in sponsors: + sponsor["url"] = sponsor_website_url(sponsor["url"]) categories = [cat for g in parsed_groups for cat in g["categories"]] cat_slugs = [cat["slug"] for cat in categories] diff --git a/website/tests/test_build.py b/website/tests/test_build.py index 7bba67be..1ccefc6b 100644 --- a/website/tests/test_build.py +++ b/website/tests/test_build.py @@ -26,6 +26,7 @@ from build import ( load_pypi_badges, load_stars, sort_entries, + sponsor_website_url, subcategory_path, ) from readme_parser import parse_readme, slugify @@ -458,6 +459,37 @@ class TestBuild: assert llms_txt != readme assert "# Contributing" not in llms_txt + def test_homepage_sponsor_link_tags_website_clicks(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## **Sponsors** + + - [Sponsor](https://sponsor.example.com/?ref=x&utm_source=awesome-python&utm_content=readme) - Sponsored tool. + + ## Projects + + **Tools** + + ## Widgets + + - [w1](https://example.com) - A widget. + + # Contributing + + Help! + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + build(tmp_path) + + index_html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + assert 'href="https://sponsor.example.com/?ref=x&utm_source=awesome-python&utm_content=website"' in index_html + assert "utm_content=readme" not in index_html + def test_build_cleans_stale_output(self, tmp_path): readme = textwrap.dedent("""\ # T @@ -1278,6 +1310,23 @@ class TestExtractGithubRepo: assert extract_github_repo("https://github.com/org") is None +# --------------------------------------------------------------------------- +# sponsor_website_url +# --------------------------------------------------------------------------- + + +class TestSponsorWebsiteUrl: + def test_replaces_readme_utm_content(self): + url = "https://example.com?utm_source=awesome-python&utm_medium=sponsorship&utm_campaign=sponsor&utm_content=readme" + assert sponsor_website_url(url) == "https://example.com?utm_source=awesome-python&utm_medium=sponsorship&utm_campaign=sponsor&utm_content=website" + + def test_keeps_path_and_other_params(self): + assert sponsor_website_url("https://example.com/landing?ref=x&utm_content=readme") == "https://example.com/landing?ref=x&utm_content=website" + + def test_leaves_untagged_url_unchanged(self): + assert sponsor_website_url("https://example.com") == "https://example.com" + + # --------------------------------------------------------------------------- # load_stars # ---------------------------------------------------------------------------