Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<meta name="description">` 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. (#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
Expand Down
10 changes: 9 additions & 1 deletion api/routers/seo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/api/test_seo_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -437,6 +438,24 @@ 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_match = re.search(r'<meta name="description" content="([^"]*)"', mpl_page)
makie_match = re.search(r'<meta name="description" content="([^"]*)"', makie_page)
assert mpl_match and makie_match
mpl_meta, makie_meta = mpl_match.group(1), makie_match.group(1)
assert mpl_meta.startswith("Basic Scatter Plot in Matplotlib (Python): ")
assert makie_meta.startswith("Basic Scatter Plot in Makie.jl (Julia): ")
assert mpl_meta != makie_meta
# the visible body copy stays the plain spec description
assert f"<p>{html.escape(spec.description)}</p>" in mpl_page

def test_no_code_no_pre_block(self) -> None:
page = self._page(code=None)
# No source block — the retrieval record has its own <pre> (class
Expand Down
Loading