From 1bc541776ed49fe3b048fd1bad093c11e26566ca Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:38:45 +0200 Subject: [PATCH 1/3] fix(seo): library-specific meta descriptions on implementation pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SEO proxy reused the spec description verbatim as the meta/OG description of every implementation page, so up to 15 library pages and their hub carried an identical snippet — 4,254 plot pages had about 334 distinct descriptions, which Bing Webmaster Tools flagged. The snippet now opens with "{title} in {library} ({language}): " before the description, trimmed to 155 characters as before. Hub, body copy and JSON-LD unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SrKzcwZBnref1sWYtdXynu --- CHANGELOG.md | 9 +++++++++ api/routers/seo.py | 10 +++++++++- docs/reference/seo.md | 8 ++++++++ tests/unit/api/test_seo_helpers.py | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a24df73e936..a7205731a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,15 @@ aggregate instead: an italic *Catalog* line at the end of the version section an ### Fixed +- **Implementation pages no longer share one meta description per spec** — the SEO + proxy reused the spec description verbatim as the `` and OG + description of every implementation page, so up to 15 library pages and their hub + carried an identical snippet; 4,254 plot pages had about 334 distinct descriptions, + which Bing Webmaster Tools flagged as "too many pages with identical meta descriptions". + The snippet now opens with the page's identity, `{title} in {library} ({language}): + {description}`, trimmed to 155 characters as before, so a searcher who typed the + library name sees it in the result. The hub keeps the plain description; body copy + and JSON-LD are unchanged. - **Infrastructure failures no longer spend a pair's generation budget** — the 3-attempt cap in `impl-generate.yml` counted every failed run alike, so the Claude outage of 2026-09-02 (03:15–03:45 UTC, every run ending in `is_error:true` with an diff --git a/api/routers/seo.py b/api/routers/seo.py index 6b16d31fd5e..853955c0460 100644 --- a/api/routers/seo.py +++ b/api/routers/seo.py @@ -692,7 +692,15 @@ def _build_impl_html(spec, impl, code: str | None, image: str) -> str: title_esc = html.escape(spec.title) lib_name_esc = html.escape(lib_name) desc_esc = html.escape(spec.description or DEFAULT_DESCRIPTION) - meta_desc_esc = html.escape(_meta_description(spec.description or DEFAULT_DESCRIPTION)) + # The meta/OG description names the library first. Reusing the spec + # description verbatim gave every implementation page of a spec (up to 15) + # and its hub the same snippet — Bing Webmaster Tools flagged the catalogue + # for "too many pages with identical meta descriptions" (2026-09-02), and a + # searcher who typed "matplotlib funnel chart" saw nothing about matplotlib + # in the result. The visible body and the JSON-LD keep the plain text. + meta_desc_esc = html.escape( + _meta_description(f"{spec.title} in {lib_name} ({lang_name}): {spec.description or DEFAULT_DESCRIPTION}") + ) image_esc = html.escape(image, quote=True) hub_url = f"https://anyplot.ai/{spec.id}" page_url = f"{hub_url}/{language_id}/{impl.library_id}" diff --git a/docs/reference/seo.md b/docs/reference/seo.md index 51ae5f04b4a..826efd3e53e 100644 --- a/docs/reference/seo.md +++ b/docs/reference/seo.md @@ -248,6 +248,14 @@ Display names (Matplotlib, Makie.jl, Apache ECharts, …) are derived from `core/constants.py` (`LANGUAGES_METADATA` / `LIBRARIES_METADATA`) — never hand-maintained in the router. +The meta/OG description is trimmed to 155 characters (`_meta_description()`). +On an implementation page it starts with the page's own identity, +`{title} in {library} ({language}): {spec description}`, so the up to 15 +library pages of a spec and its hub no longer share one snippet — Bing +Webmaster Tools flagged that duplication in 2026-09 — and a searcher who +typed the library name sees it in the result. The hub keeps the plain spec +description; body copy and JSON-LD carry the full text on both. + ## What a crawler sees of the plot Two different images exist per implementation, and the bot page carries both, diff --git a/tests/unit/api/test_seo_helpers.py b/tests/unit/api/test_seo_helpers.py index 494937a599e..3ddf3278b60 100644 --- a/tests/unit/api/test_seo_helpers.py +++ b/tests/unit/api/test_seo_helpers.py @@ -437,6 +437,22 @@ def test_jsonld_without_render_keeps_card_image(self) -> None: source = _extract_jsonld(page)["@graph"][1] assert source["image"] == "https://api.anyplot.ai/og/card.png" + def test_meta_description_names_the_library(self) -> None: + """Every implementation page of a spec used to carry the spec description + verbatim, so up to 16 pages shared one snippet (Bing flagged it, 2026-09).""" + mpl = _mock_impl("matplotlib", "python") + makie = _mock_impl("makie", "julia") + spec = _mock_spec([mpl, makie]) + mpl_page = _build_impl_html(spec, mpl, "code()", "https://api.anyplot.ai/og/card.png") + makie_page = _build_impl_html(spec, makie, "code()", "https://api.anyplot.ai/og/card.png") + mpl_meta = re.search(r' None: page = self._page(code=None) # No source block — the retrieval record has its own
 (class

From 3ed9e213cf8148873642469287a070f8f48a77df Mon Sep 17 00:00:00 2001
From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:43:58 +0200
Subject: [PATCH 2/3] test(seo): assert the regex matches and the rendered body
 copy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Review feedback: guard re.search() before .group() and check the escaped

body instead of a substring that JSON-LD also carries. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SrKzcwZBnref1sWYtdXynu --- tests/unit/api/test_seo_helpers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit/api/test_seo_helpers.py b/tests/unit/api/test_seo_helpers.py index 3ddf3278b60..138a443a8ce 100644 --- a/tests/unit/api/test_seo_helpers.py +++ b/tests/unit/api/test_seo_helpers.py @@ -4,6 +4,7 @@ Directly tests the pure helper functions in api/routers/seo.py. """ +import html import json import re from datetime import datetime @@ -445,13 +446,15 @@ def test_meta_description_names_the_library(self) -> None: spec = _mock_spec([mpl, makie]) mpl_page = _build_impl_html(spec, mpl, "code()", "https://api.anyplot.ai/og/card.png") makie_page = _build_impl_html(spec, makie, "code()", "https://api.anyplot.ai/og/card.png") - mpl_meta = re.search(r'{html.escape(spec.description)}

" in mpl_page def test_no_code_no_pre_block(self) -> None: page = self._page(code=None) From 841e8e993436d822c90d7b0d59404e0d4347426f Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:54:55 +0200 Subject: [PATCH 3/3] docs(changelog): add PR reference for #11203 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SrKzcwZBnref1sWYtdXynu --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7205731a9a..726db080551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ aggregate instead: an italic *Catalog* line at the end of the version section an The snippet now opens with the page's identity, `{title} in {library} ({language}): {description}`, trimmed to 155 characters as before, so a searcher who typed the library name sees it in the result. The hub keeps the plain description; body copy - and JSON-LD are unchanged. + and JSON-LD are unchanged. (#11203) - **Infrastructure failures no longer spend a pair's generation budget** — the 3-attempt cap in `impl-generate.yml` counted every failed run alike, so the Claude outage of 2026-09-02 (03:15–03:45 UTC, every run ending in `is_error:true` with an