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