diff --git a/DESIGN.md b/DESIGN.md index 69b99542..8cad2e73 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -162,7 +162,7 @@ The component vocabulary is small and table-led. Source of truth: `website/stati - **Hero**. Magazine-cover headline, dark earthy ground, kicker and proof microcopy, primary CTA button using `--hero-btn-start` / `--hero-btn-end`. Subtle grid plus slow sheen. Respects `prefers-reduced-motion`. - **Jump links**. One per use case in a category hero, plus one for the guide. Underlined like the intro's links, with a trailing ↓. Never tag-styled: tags filter the table or open another page, jump links only scroll. - **Guide band**. Everything in a category intro after its "How to choose" list. It sits below the table on the hero's dark gradient, with its H2 above the text. -- **Sponsor band**. Sits below the hero on the home page, on `--bg-paper-strong`. Editorial layout, not a logo wall. Sponsor links share the global accent treatment. +- **Sponsor band**. Sits below the hero on the home page and category pages, on `--bg-paper-strong`. Editorial layout, not a logo wall. Sponsor links share the global accent treatment. - **CTA**. Warm `--cta-bg`, full-bleed within shell. The button itself uses accent tokens. - **Footer**. Dark warm charcoal, part of the same system. Footer links share the global hover and focus treatment. - **Search**. Pill input with `--search-inset` interior and `--search-focus-ring` focus ring. Focus shadow uses `--search-focus-shadow`. diff --git a/README.md b/README.md index db0ac398..83107e9f 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ 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. -> The **#10 most-starred repo on GitHub**. Put your product in front of Python developers. [Become a sponsor](SPONSORSHIP.md). +> **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/). ## Categories diff --git a/SPONSORSHIP.md b/SPONSORSHIP.md index 1e27bee5..f788606e 100644 --- a/SPONSORSHIP.md +++ b/SPONSORSHIP.md @@ -21,17 +21,17 @@ Your sponsorship puts your product in front of developers at the exact moment th ## Sponsorship Tiers -### Headline Sponsor - $500/month +### Headline Sponsor - $500/month or $4,500/year - Large logo and one-line description (max 120 characters) pinned at the very top of the README, above all project entries - Logo link in the sponsor section of [awesome-python.com](https://awesome-python.com/) -### Featured Sponsor - $200/month +### Featured Sponsor - $200/month or $1,800/year -- Text entry (`[Name](URL) - Description.`, max 120 characters) pinned at the top of the README, directly below Headline sponsors -- Text link in the sponsor section of [awesome-python.com](https://awesome-python.com/) +- Small logo and text entry (`[Name](URL) - Description.`, max 120 characters) pinned at the top of the README, directly below Headline sponsors +- Logo and text link in the sponsor section of [awesome-python.com](https://awesome-python.com/) -### Indie Dev Sponsor - $99/month or $249/quarter +### Indie Dev Sponsor - $99/month, $249/quarter, or $891/year - Lower-cost tier for indie developers, solo founders, and one-person companies only - Text entry (`[Name](URL) - Description.`, max 120 characters) in the README sponsor section, directly below Featured sponsors @@ -43,13 +43,15 @@ Indie Dev Sponsor is available only to founder-led (one-person company) products Email [sponsorship@awesome-python.com](mailto:sponsorship@awesome-python.com?subject=Awesome%20Python%20Sponsorship) with: -- **Tier:** Headline Sponsor ($500/mo), Featured Sponsor ($200/mo), or Indie Dev Sponsor ($99/mo or $249/quarter) -- **Content:** Product name, URL, logo, and description (Headline tier) or `[Name](URL) - Description.` entry (Featured or Indie Dev tier) -- **Duration:** 1, 3, 6 months, or longer +- **Tier:** Headline Sponsor ($500/mo or $4,500/year), Featured Sponsor ($200/mo or $1,800/year), or Indie Dev Sponsor ($99/mo, $249/quarter, or $891/year) +- **Content:** Product name, URL, logo, and description (Headline tier), logo and `[Name](URL) - Description.` entry (Featured tier), or `[Name](URL) - Description.` entry (Indie Dev tier) +- **Duration:** 1, 3, or 6 months, or 1 year (3 months free) - **Payment method:** card via [Open Collective](https://opencollective.com/awesome-python), US bank transfer (ACH/wire), or PayPal 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..24b35b72 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] @@ -881,6 +894,7 @@ def build(repo_root: Path) -> None: parent_category=parent_category, group_categories=group_categories, category_json_ld=category_json_ld, + sponsors=sponsors, ), encoding="utf-8", ) diff --git a/website/static/main.js b/website/static/main.js index 72dd4e0f..8ca095ba 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -399,6 +399,13 @@ document.addEventListener("click", function (e) { } }); +// Mailto clicks leave no page view, so the sponsor funnel needs its own event +document.addEventListener("click", function (e) { + const link = e.target.closest('a[href^="mailto:sponsorship@"]'); + if (!link || typeof gtag !== "function") return; + gtag("event", "sponsor_contact", { tier: link.dataset.tier }); +}); + if (location.hash) { const linkedRow = document.getElementById(decodeURIComponent(location.hash.slice(1))); if (linkedRow && linkedRow.matches("tr.row")) openEntryRow(linkedRow); diff --git a/website/templates/_sponsor_band.html b/website/templates/_sponsor_band.html new file mode 100644 index 00000000..a47dabab --- /dev/null +++ b/website/templates/_sponsor_band.html @@ -0,0 +1,25 @@ +{% if sponsors %} + +{% endif %} diff --git a/website/templates/category.html b/website/templates/category.html index 4563dc4f..6b867269 100644 --- a/website/templates/category.html +++ b/website/templates/category.html @@ -208,6 +208,8 @@ {% endmacro %} +{% include "_sponsor_band.html" %} +
{% if entry_groups %} diff --git a/website/templates/index.html b/website/templates/index.html index 4cb99d4c..4b024c3c 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -70,31 +70,7 @@ {% endblock %} {% block content %} -{% if sponsors %} - -{% endif %} +{% include "_sponsor_band.html" %}
diff --git a/website/templates/sponsorship.html b/website/templates/sponsorship.html index 34de3517..46d546d8 100644 --- a/website/templates/sponsorship.html +++ b/website/templates/sponsorship.html @@ -32,7 +32,8 @@

The #10 most-starred repository on GitHub, and the list Python developers check when choosing what to use. Your sponsorship puts your - product in front of them at the moment of decision. + product in front of them at the moment of decision. Headline $500, + Featured $200, Indie Dev from $99 a month.

{% if hero_stats %} @@ -45,6 +46,7 @@
Email sponsorship@awesome-python.com @@ -114,7 +116,7 @@

Headline Sponsor

$500 - / month + / month or $4,500 / year

Logo pinned at the top of the README. Logo on the website. @@ -128,6 +130,7 @@ Email about Headline tier @@ -136,21 +139,25 @@

Featured Sponsor

$200 - / month + / month or $1,800 / year

- Text link pinned at the top of the README. Text link on the website. + Small logo and text link pinned at the top of the README. Logo and + text link on the website.

  • - Text entry ([Name](URL) - Description., max 120 - characters) pinned at the top of the README, directly below - Headline sponsors. + Small logo and text entry + ([Name](URL) - Description., max 120 characters) + pinned at the top of the README, directly below Headline sponsors. +
  • +
  • + Logo and text link in the sponsor section of awesome-python.com.
  • -
  • Text link in the sponsor section of awesome-python.com.
Email about Featured tier @@ -159,7 +166,7 @@

Indie Dev Sponsor

$99 - / month or $249 / quarter + / month, $249 / quarter, or $891 / year

Lower-cost text link for eligible indie developers and solo @@ -180,6 +187,7 @@ Email about Indie Dev tier @@ -225,6 +233,7 @@ Email sponsorship@awesome-python.com with the four items below. @@ -233,21 +242,22 @@

Tier
- Headline Sponsor ($500/mo), Featured Sponsor ($200/mo), or Indie Dev - Sponsor ($99/mo or $249/quarter). + Headline Sponsor ($500/mo or $4,500/year), Featured Sponsor + ($200/mo or $1,800/year), or Indie Dev Sponsor ($99/mo, $249/quarter, + or $891/year).
Content
- Product name, URL, logo, and description (Headline tier), or - [Name](URL) - Description. entry (Featured or Indie Dev - tier). + Product name, URL, logo, and description (Headline tier), logo and + [Name](URL) - Description. entry (Featured tier), or + [Name](URL) - Description. entry (Indie Dev tier).
Duration
-
1, 3, 6 months, or longer.
+
1, 3, or 6 months, or 1 year (3 months free).
Payment method
@@ -261,6 +271,7 @@
Email sponsorship@awesome-python.com diff --git a/website/tests/test_build.py b/website/tests/test_build.py index 7bba67be..eed15fdc 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,68 @@ 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_category_page_shows_sponsor_band(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## **Sponsors** + + - [Sponsor](https://sponsor.example.com) - 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) + + category_html = (tmp_path / "website" / "output" / "categories" / "widgets" / "index.html").read_text(encoding="utf-8") + assert 'href="https://sponsor.example.com"' in category_html + assert '' in category_html + def test_build_cleans_stale_output(self, tmp_path): readme = textwrap.dedent("""\ # T @@ -1278,6 +1341,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 # ---------------------------------------------------------------------------