mirror of
https://github.com/vinta/awesome-python.git
synced 2026-10-02 08:23:10 +08:00
fix: make tags link to category pages instead of filtering in place
Clicking a category or group tag filtered the homepage table in place while rewriting the address bar to the category URL, so one URL rendered two different pages and readers never reached category pages with their intros and guides; category pages also showed a redundant self-referential filter bar. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -678,12 +678,7 @@ def build(repo_root: Path) -> None:
|
||||
filter_urls: dict[str, str] = dict(category_urls)
|
||||
for group in parsed_groups:
|
||||
filter_urls[group["name"]] = group_path(group["slug"])
|
||||
for entry in entries:
|
||||
for sub in entry.get("subcategories", []):
|
||||
filter_urls[sub["value"]] = sub["url"]
|
||||
builtin_entries = [e for e in entries if e.get("source_type") == BUILTIN_FILTER]
|
||||
if builtin_entries:
|
||||
filter_urls[BUILTIN_FILTER] = BUILTIN_PATH
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(website / "templates"),
|
||||
@@ -696,7 +691,6 @@ def build(repo_root: Path) -> None:
|
||||
shutil.rmtree(site_dir)
|
||||
site_dir.mkdir(parents=True)
|
||||
|
||||
filter_urls_json = json.dumps(filter_urls, sort_keys=True, ensure_ascii=False).replace("</", "<\\/")
|
||||
homepage_json_ld = json.dumps(
|
||||
build_homepage_json_ld(entries, len(categories)),
|
||||
ensure_ascii=False,
|
||||
@@ -715,7 +709,6 @@ def build(repo_root: Path) -> None:
|
||||
sponsors=sponsors,
|
||||
category_urls=category_urls,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
homepage_json_ld=homepage_json_ld,
|
||||
),
|
||||
encoding="utf-8",
|
||||
@@ -762,7 +755,6 @@ def build(repo_root: Path) -> None:
|
||||
category_urls=category_urls,
|
||||
current_path=current_path,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
parent_category=parent_category,
|
||||
group_categories=group_categories,
|
||||
category_json_ld=category_json_ld,
|
||||
|
||||
+6
-85
@@ -10,15 +10,10 @@ const defaultSort =
|
||||
table && table.dataset.defaultSort === "editorial"
|
||||
? { col: "editorial", order: "asc" }
|
||||
: { col: "downloads", order: "desc" };
|
||||
let activeFilter = null;
|
||||
let activeSort = defaultSort;
|
||||
const searchInput = document.querySelector(".search");
|
||||
const filterBar = document.querySelector(".filter-bar");
|
||||
const filterValue = document.querySelector(".filter-value");
|
||||
const filterClear = document.querySelector(".filter-clear");
|
||||
const noResults = document.querySelector(".no-results");
|
||||
const rows = document.querySelectorAll(".table tbody tr.row");
|
||||
const tags = document.querySelectorAll(".tag");
|
||||
const tbody = document.querySelector(".table tbody");
|
||||
const groupRows = document.querySelectorAll(".table tbody tr.group-row");
|
||||
|
||||
@@ -145,7 +140,7 @@ function collapseAll() {
|
||||
|
||||
function applyFilters() {
|
||||
const query = searchInput ? searchInput.value.toLowerCase().trim() : "";
|
||||
const descRowsVisible = !isIndexDocument || activeFilter !== null;
|
||||
const descRowsVisible = !isIndexDocument;
|
||||
let visibleCount = 0;
|
||||
|
||||
collapseAll();
|
||||
@@ -153,12 +148,7 @@ function applyFilters() {
|
||||
rows.forEach(function (row) {
|
||||
let show = true;
|
||||
|
||||
if (activeFilter) {
|
||||
const rowTags = row.dataset.tags;
|
||||
show = rowTags ? rowTags.split("||").includes(activeFilter) : false;
|
||||
}
|
||||
|
||||
if (show && query) {
|
||||
if (query) {
|
||||
if (!row._searchText) {
|
||||
let text = row.textContent.toLowerCase();
|
||||
if (row._descRow) {
|
||||
@@ -200,35 +190,12 @@ function applyFilters() {
|
||||
|
||||
if (noResults) noResults.hidden = visibleCount > 0;
|
||||
|
||||
tags.forEach(function (tag) {
|
||||
tag.classList.toggle("active", activeFilter === tag.dataset.value);
|
||||
});
|
||||
|
||||
if (filterBar) {
|
||||
if (activeFilter) {
|
||||
filterBar.classList.add("visible");
|
||||
if (filterValue) filterValue.textContent = activeFilter;
|
||||
} else {
|
||||
filterBar.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
updateURL();
|
||||
}
|
||||
|
||||
const filterUrlsScript = document.getElementById("filter-urls");
|
||||
const filterToUrl = filterUrlsScript
|
||||
? JSON.parse(filterUrlsScript.textContent)
|
||||
: {};
|
||||
|
||||
const isIndexDocument =
|
||||
location.pathname === "/" || location.pathname === "/index.html";
|
||||
|
||||
const urlToFilter = {};
|
||||
Object.keys(filterToUrl).forEach(function (k) {
|
||||
urlToFilter[filterToUrl[k]] = k;
|
||||
});
|
||||
|
||||
function buildQueryString() {
|
||||
const params = new URLSearchParams();
|
||||
const query = searchInput ? searchInput.value.trim() : "";
|
||||
@@ -243,9 +210,7 @@ function buildQueryString() {
|
||||
|
||||
function updateURL() {
|
||||
if (!isIndexDocument) return;
|
||||
const path =
|
||||
activeFilter && filterToUrl[activeFilter] ? filterToUrl[activeFilter] : "/";
|
||||
history.replaceState(null, "", path + buildQueryString());
|
||||
history.replaceState(null, "", "/" + buildQueryString());
|
||||
}
|
||||
|
||||
function getSortValue(row, col) {
|
||||
@@ -340,8 +305,8 @@ function updateSortIndicators() {
|
||||
// Expand/collapse: event delegation on tbody
|
||||
if (tbody) {
|
||||
tbody.addEventListener("click", function (e) {
|
||||
// Don't toggle if clicking a link or tag button
|
||||
if (e.target.closest("a") || e.target.closest(".tag")) return;
|
||||
// Don't toggle if clicking a link
|
||||
if (e.target.closest("a")) return;
|
||||
|
||||
let row = e.target.closest("tr.row");
|
||||
if (!row) {
|
||||
@@ -370,36 +335,6 @@ if (tbody) {
|
||||
});
|
||||
}
|
||||
|
||||
tags.forEach(function (tag) {
|
||||
tag.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const value = tag.dataset.value;
|
||||
const url = tag.dataset.url;
|
||||
if (isIndexDocument) {
|
||||
activeFilter = activeFilter === value ? null : value;
|
||||
if (activeFilter && url) {
|
||||
history.pushState(null, "", url + buildQueryString());
|
||||
} else {
|
||||
history.pushState(null, "", "/" + buildQueryString());
|
||||
}
|
||||
applyFilters();
|
||||
} else if (url) {
|
||||
window.location.href = url + "#library-index";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (filterClear) {
|
||||
filterClear.addEventListener("click", function () {
|
||||
if (!isIndexDocument) {
|
||||
window.location.href = "/#library-index";
|
||||
return;
|
||||
}
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
const noResultsClear = document.querySelector(".no-results-clear");
|
||||
if (noResultsClear) {
|
||||
noResultsClear.addEventListener("click", function () {
|
||||
@@ -408,7 +343,6 @@ if (noResultsClear) {
|
||||
return;
|
||||
}
|
||||
if (searchInput) searchInput.value = "";
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
@@ -451,7 +385,6 @@ if (searchInput) {
|
||||
}
|
||||
if (e.key === "Escape" && document.activeElement === searchInput) {
|
||||
searchInput.value = "";
|
||||
activeFilter = null;
|
||||
applyFilters();
|
||||
searchInput.blur();
|
||||
}
|
||||
@@ -512,20 +445,8 @@ if (backToTop) {
|
||||
) {
|
||||
activeSort = { col: sort, order: order };
|
||||
}
|
||||
const matched = urlToFilter[location.pathname];
|
||||
if (matched) activeFilter = matched;
|
||||
if (q || activeFilter || sort) {
|
||||
if (q || sort) {
|
||||
sortRows();
|
||||
}
|
||||
if (activeFilter) {
|
||||
applyFilters();
|
||||
}
|
||||
updateSortIndicators();
|
||||
})();
|
||||
|
||||
window.addEventListener("popstate", function () {
|
||||
if (!isIndexDocument) return;
|
||||
const matched = urlToFilter[location.pathname];
|
||||
activeFilter = matched || null;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
@@ -265,8 +265,7 @@ kbd {
|
||||
|
||||
.hero-topbar-link:active,
|
||||
.hero-action:active,
|
||||
.tag:active,
|
||||
.filter-clear:active {
|
||||
.tag:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@@ -436,7 +435,6 @@ kbd {
|
||||
.hero-topbar-link:focus-visible,
|
||||
.hero-category-link:focus-visible,
|
||||
.search:focus-visible,
|
||||
.filter-clear:focus-visible,
|
||||
.tag:focus-visible,
|
||||
.jump-link:focus-visible,
|
||||
.back-to-top:focus-visible,
|
||||
@@ -771,51 +769,6 @@ kbd {
|
||||
0 1.6rem 3rem -2rem var(--search-focus-shadow);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-height: 2.3rem;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-soft);
|
||||
opacity: 0;
|
||||
transform: translateY(-0.4rem);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.filter-bar.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.filter-bar strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-paper);
|
||||
color: var(--ink-soft);
|
||||
padding: 0.42rem 0.82rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
color: var(--ink);
|
||||
background: var(--accent-soft);
|
||||
border-color: oklch(68% 0.08 58 / 0.5);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
{% macro entry_rows(entry, index) %}
|
||||
<tr
|
||||
class="row"
|
||||
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Stdlib' %}||Stdlib{% endif %}"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="expand-{{ index }}"
|
||||
@@ -124,7 +123,7 @@
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ category_urls[subcat.value.split(' > ')[0]] }}#{{ subcat.slug }}">
|
||||
{{ subcat.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -132,8 +131,6 @@
|
||||
<a
|
||||
class="tag{% if category_urls[cat] == current_path %} active{% endif %}"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
@@ -142,8 +139,6 @@
|
||||
<a
|
||||
class="tag tag-group{% if group_url == current_path %} active{% endif %}"
|
||||
href="{{ group_url }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ group_url }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</a>
|
||||
@@ -152,8 +147,6 @@
|
||||
<a
|
||||
class="tag tag-source{% if '/categories/built-in/' == current_path %} active{% endif %}"
|
||||
href="/categories/built-in/"
|
||||
data-value="Stdlib"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Stdlib
|
||||
</a>
|
||||
@@ -211,7 +204,6 @@
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
{% if entry_groups %}
|
||||
@@ -255,12 +247,6 @@
|
||||
aria-label="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-bar" aria-live="polite">
|
||||
<span>Filtering for <strong class="filter-value"></strong></span>
|
||||
<button class="filter-clear" aria-label="Clear filter">
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Results</h2>
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
<div>
|
||||
@@ -132,12 +131,6 @@
|
||||
aria-label="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-bar" aria-live="polite">
|
||||
<span>Filtering for <strong class="filter-value"></strong></span>
|
||||
<button class="filter-clear" aria-label="Clear filter">
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Results</h2>
|
||||
@@ -175,7 +168,6 @@
|
||||
{% for entry in entries %}
|
||||
<tr
|
||||
class="row"
|
||||
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Stdlib' %}||Stdlib{% endif %}"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="expand-{{ loop.index }}"
|
||||
@@ -221,23 +213,19 @@
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<a class="tag" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
<a class="tag" href="{{ category_urls[subcat.value.split(' > ')[0]] }}#{{ subcat.slug }}">
|
||||
{{ subcat.name }}
|
||||
</a>
|
||||
{% endfor %} {% for cat in entry.categories %}
|
||||
<a
|
||||
class="tag"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<a
|
||||
class="tag tag-group"
|
||||
href="{{ filter_urls[entry.groups[0]] }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ filter_urls[entry.groups[0]] }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</a>
|
||||
@@ -245,8 +233,6 @@
|
||||
<a
|
||||
class="tag tag-source"
|
||||
href="/categories/built-in/"
|
||||
data-value="Stdlib"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Stdlib
|
||||
</a>
|
||||
|
||||
@@ -280,7 +280,6 @@ class TestBuild:
|
||||
parser.feed(category_html)
|
||||
|
||||
assert 'href="/categories/widgets/"' in index_html
|
||||
assert 'data-value="Widgets"' in index_html
|
||||
assert parser.title.strip() == "Python Widgets Libraries - Awesome Python"
|
||||
assert parser.meta_by_name["description"] == "Widget libraries. Also see awesome-widgets. Explore 2 curated Python projects in Widgets."
|
||||
assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/"
|
||||
@@ -816,75 +815,6 @@ class TestBuild:
|
||||
{"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"},
|
||||
]
|
||||
|
||||
def test_index_embeds_filter_urls_json(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
**AI & ML**
|
||||
|
||||
## Deep Learning
|
||||
|
||||
- [dl1](https://example.com/dl1) - DL.
|
||||
|
||||
## Machine Learning
|
||||
|
||||
- Classical
|
||||
|
||||
- [ml1](https://example.com/ml1) - ML.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
marker = '<script type="application/json" id="filter-urls">'
|
||||
assert marker in index_html
|
||||
start = index_html.index(marker) + len(marker)
|
||||
end = index_html.index("</script>", start)
|
||||
data = json.loads(index_html[start:end])
|
||||
|
||||
assert data["Deep Learning"] == "/categories/deep-learning/"
|
||||
assert data["Machine Learning"] == "/categories/machine-learning/"
|
||||
assert data["AI & ML"] == "/categories/ai-ml/"
|
||||
assert data["Machine Learning > Classical"] == "/categories/machine-learning/classical/"
|
||||
|
||||
def test_filter_urls_json_escapes_closing_script_tag(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
## Projects
|
||||
|
||||
## Sneaky </script><script>x=1</script>
|
||||
|
||||
- [a](https://example.com) - A.
|
||||
|
||||
# Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
self._copy_real_templates(tmp_path)
|
||||
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
|
||||
build(tmp_path)
|
||||
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
marker = '<script type="application/json" id="filter-urls">'
|
||||
start = index_html.index(marker) + len(marker)
|
||||
end = index_html.index("</script>", start)
|
||||
block = index_html[start:end]
|
||||
assert "</script>" not in block
|
||||
data = json.loads(block)
|
||||
assert any("Sneaky" in key for key in data)
|
||||
|
||||
def test_build_creates_group_pages(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
@@ -925,7 +855,7 @@ class TestBuild:
|
||||
assert "wf1" in web_dev
|
||||
assert "dl1" not in web_dev
|
||||
|
||||
def test_tag_buttons_have_data_url(self, tmp_path):
|
||||
def test_tags_link_to_pages_and_subcategory_anchors(self, tmp_path):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
@@ -950,11 +880,14 @@ class TestBuild:
|
||||
site = tmp_path / "website" / "output"
|
||||
index_html = (site / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'data-value="Deep Learning"' in index_html
|
||||
assert 'data-url="/categories/deep-learning/"' in index_html
|
||||
assert 'data-value="AI & ML"' in index_html or 'data-value="AI & ML"' in index_html
|
||||
assert 'data-url="/categories/ai-ml/"' in index_html
|
||||
assert 'data-url="/categories/deep-learning/vision/"' in index_html
|
||||
category_html = (site / "categories" / "deep-learning" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
for html in (index_html, category_html):
|
||||
assert 'href="/categories/deep-learning/#vision"' in html
|
||||
assert 'href="/categories/ai-ml/"' in html
|
||||
assert "data-url=" not in html
|
||||
assert 'href="/categories/deep-learning/"' in index_html
|
||||
assert 'id="vision"' in category_html
|
||||
|
||||
_REDIRECT_README = textwrap.dedent("""\
|
||||
# Awesome Python
|
||||
|
||||
Reference in New Issue
Block a user