Skip to content

Commit bbdddc3

Browse files
feat(seo): add basic SEO files for Google Search Console (#528)
## Summary - Add `robots.txt` with sitemap reference - Add `sitemap.xml` with main page URL - Add Open Graph and Twitter Card meta tags to `index.html` - Add `og-image.png` (1200x630px) for social media previews - Update meta description to match website tagline ## Files Added/Changed | File | Change | |------|--------| | `app/public/robots.txt` | New - crawl rules + sitemap reference | | `app/public/sitemap.xml` | New - main page URL | | `app/index.html` | Updated - OG, Twitter Card, canonical tags | | `app/public/og-image.png` | New - social media preview image | ## After Deployment 1. Submit sitemap in Google Search Console: `https://pyplots.ai/sitemap.xml` 2. Request URL inspection for `https://pyplots.ai/` 3. Test OG tags at https://www.opengraph.xyz/
1 parent 6554ebe commit bbdddc3

File tree

8 files changed

+108
-51
lines changed

8 files changed

+108
-51
lines changed

app/index.html

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,25 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<meta name="description" content="AI-powered Python plotting examples that work with YOUR data" />
8-
<title>pyplots - AI-Powered Plotting Examples</title>
7+
<meta name="description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
8+
<title>pyplots.ai</title>
9+
10+
<!-- Canonical -->
11+
<link rel="canonical" href="https://pyplots.ai/" />
12+
13+
<!-- Open Graph -->
14+
<meta property="og:type" content="website" />
15+
<meta property="og:url" content="https://pyplots.ai/" />
16+
<meta property="og:title" content="pyplots.ai" />
17+
<meta property="og:description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
18+
<meta property="og:image" content="https://pyplots.ai/og-image.png" />
19+
<meta property="og:site_name" content="pyplots.ai" />
20+
21+
<!-- Twitter Card -->
22+
<meta name="twitter:card" content="summary_large_image" />
23+
<meta name="twitter:title" content="pyplots.ai" />
24+
<meta name="twitter:description" content="library-agnostic, ai-powered python plotting examples. automatically generated, tested, and maintained." />
25+
<meta name="twitter:image" content="https://pyplots.ai/og-image.png" />
926
<link rel="preconnect" href="https://fonts.googleapis.com">
1027
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1128
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&display=swap" rel="stylesheet">

app/public/og-image.png

628 KB
Loading

app/public/robots.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://pyplots.ai/sitemap.xml

app/public/sitemap.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
<url>
4+
<loc>https://pyplots.ai/</loc>
5+
<lastmod>2025-12-07</lastmod>
6+
<changefreq>weekly</changefreq>
7+
<priority>1.0</priority>
8+
</url>
9+
</urlset>

plots/bokeh/scatter/scatter-color-groups/default.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@
1414
np.random.seed(42)
1515
n_per_group = 50
1616

17-
data = pd.DataFrame({
18-
"sepal_length": np.concatenate([
19-
np.random.normal(5.0, 0.35, n_per_group),
20-
np.random.normal(5.9, 0.50, n_per_group),
21-
np.random.normal(6.6, 0.60, n_per_group),
22-
]),
23-
"sepal_width": np.concatenate([
24-
np.random.normal(3.4, 0.38, n_per_group),
25-
np.random.normal(2.8, 0.30, n_per_group),
26-
np.random.normal(3.0, 0.30, n_per_group),
27-
]),
28-
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
29-
})
17+
data = pd.DataFrame(
18+
{
19+
"sepal_length": np.concatenate(
20+
[
21+
np.random.normal(5.0, 0.35, n_per_group),
22+
np.random.normal(5.9, 0.50, n_per_group),
23+
np.random.normal(6.6, 0.60, n_per_group),
24+
]
25+
),
26+
"sepal_width": np.concatenate(
27+
[
28+
np.random.normal(3.4, 0.38, n_per_group),
29+
np.random.normal(2.8, 0.30, n_per_group),
30+
np.random.normal(3.0, 0.30, n_per_group),
31+
]
32+
),
33+
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
34+
}
35+
)
3036

3137
# Color palette (from style guide)
3238
colors = ["#306998", "#FFD43B", "#DC2626", "#059669", "#8B5CF6", "#F97316"]

plots/matplotlib/scatter/scatter-color-groups/default.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,25 @@
1212
np.random.seed(42)
1313
n_per_group = 50
1414

15-
data = pd.DataFrame({
16-
"sepal_length": np.concatenate([
17-
np.random.normal(5.0, 0.35, n_per_group),
18-
np.random.normal(5.9, 0.50, n_per_group),
19-
np.random.normal(6.6, 0.60, n_per_group),
20-
]),
21-
"sepal_width": np.concatenate([
22-
np.random.normal(3.4, 0.38, n_per_group),
23-
np.random.normal(2.8, 0.30, n_per_group),
24-
np.random.normal(3.0, 0.30, n_per_group),
25-
]),
26-
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
27-
})
15+
data = pd.DataFrame(
16+
{
17+
"sepal_length": np.concatenate(
18+
[
19+
np.random.normal(5.0, 0.35, n_per_group),
20+
np.random.normal(5.9, 0.50, n_per_group),
21+
np.random.normal(6.6, 0.60, n_per_group),
22+
]
23+
),
24+
"sepal_width": np.concatenate(
25+
[
26+
np.random.normal(3.4, 0.38, n_per_group),
27+
np.random.normal(2.8, 0.30, n_per_group),
28+
np.random.normal(3.0, 0.30, n_per_group),
29+
]
30+
),
31+
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
32+
}
33+
)
2834

2935
# Color palette (colorblind safe from style guide)
3036
colors = ["#306998", "#FFD43B", "#DC2626"]

plots/plotnine/point/scatter-color-groups/default.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,25 @@
1212
np.random.seed(42)
1313
n_per_group = 50
1414

15-
data = pd.DataFrame({
16-
"sepal_length": np.concatenate([
17-
np.random.normal(5.0, 0.35, n_per_group),
18-
np.random.normal(5.9, 0.50, n_per_group),
19-
np.random.normal(6.6, 0.60, n_per_group),
20-
]),
21-
"sepal_width": np.concatenate([
22-
np.random.normal(3.4, 0.38, n_per_group),
23-
np.random.normal(2.8, 0.30, n_per_group),
24-
np.random.normal(3.0, 0.30, n_per_group),
25-
]),
26-
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
27-
})
15+
data = pd.DataFrame(
16+
{
17+
"sepal_length": np.concatenate(
18+
[
19+
np.random.normal(5.0, 0.35, n_per_group),
20+
np.random.normal(5.9, 0.50, n_per_group),
21+
np.random.normal(6.6, 0.60, n_per_group),
22+
]
23+
),
24+
"sepal_width": np.concatenate(
25+
[
26+
np.random.normal(3.4, 0.38, n_per_group),
27+
np.random.normal(2.8, 0.30, n_per_group),
28+
np.random.normal(3.0, 0.30, n_per_group),
29+
]
30+
),
31+
"species": ["setosa"] * n_per_group + ["versicolor"] * n_per_group + ["virginica"] * n_per_group,
32+
}
33+
)
2834

2935
# Color palette (from style guide)
3036
colors = ["#306998", "#FFD43B", "#DC2626"]

tests/unit/prompts/test_prompts.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"plotnine.md",
3939
"pygal.md",
4040
"highcharts.md",
41+
"letsplot.md",
4142
]
4243

4344

@@ -81,15 +82,21 @@ def quality_criteria_content(self) -> str:
8182
return (PROMPTS_DIR / "quality-criteria.md").read_text()
8283

8384
def test_plot_generator_has_required_sections(self, plot_generator_content: str) -> None:
84-
"""Plot generator should have Role, Task, Rules sections."""
85-
required_sections = ["## Role", "## Task", "## Rules", "## Output"]
85+
"""Plot generator should have Role, Task, Output sections."""
86+
# Core sections at level 2
87+
required_sections = ["## Role", "## Task", "## Output"]
8688
for section in required_sections:
8789
assert section in plot_generator_content, f"Missing section: {section}"
90+
# Rules can be at level 2 or 3 (### Rules under ## Output)
91+
assert "Rules" in plot_generator_content, "Missing Rules section"
8892

8993
def test_plot_generator_has_code_template(self, plot_generator_content: str) -> None:
9094
"""Plot generator should include a code template."""
9195
assert "```python" in plot_generator_content, "Missing Python code template"
92-
assert "def create_plot" in plot_generator_content, "Missing create_plot function template"
96+
# KISS style: simple scripts with comments, not functions
97+
assert "# Create plot" in plot_generator_content or "plt.savefig" in plot_generator_content, (
98+
"Missing plot creation example"
99+
)
93100

94101
def test_quality_criteria_has_scoring_section(self, quality_criteria_content: str) -> None:
95102
"""Quality criteria should have scoring information."""
@@ -107,8 +114,9 @@ def test_library_prompt_has_required_sections(self, filename: str) -> None:
107114
content = (LIBRARY_PROMPTS_DIR / filename).read_text()
108115
library_name = filename.replace(".md", "")
109116

110-
# Check for header
111-
assert f"# {library_name}" in content.lower(), f"Missing header for {library_name}"
117+
# Check for header (normalize by removing hyphens for comparison)
118+
content_normalized = content.lower().replace("-", "")
119+
assert f"# {library_name}" in content_normalized, f"Missing header for {library_name}"
112120

113121
# Check for import section
114122
assert "## Import" in content or "import" in content.lower(), f"Missing import section in {filename}"
@@ -117,12 +125,13 @@ def test_library_prompt_has_required_sections(self, filename: str) -> None:
117125
assert "```python" in content, f"Missing Python code examples in {filename}"
118126

119127
@pytest.mark.parametrize("filename", EXPECTED_LIBRARY_PROMPTS)
120-
def test_library_prompt_has_return_type(self, filename: str) -> None:
121-
"""Each library prompt should specify return type."""
128+
def test_library_prompt_has_save_section(self, filename: str) -> None:
129+
"""Each library prompt should show how to save the plot."""
122130
content = (LIBRARY_PROMPTS_DIR / filename).read_text()
123-
# Either explicit return type section or type hint in code
124-
has_return_type = "## Return Type" in content or "-> " in content
125-
assert has_return_type, f"Missing return type specification in {filename}"
131+
# KISS style: prompts show how to save, not function return types
132+
save_patterns = ["## Save", "savefig", "save(", "write_image", "save_screenshot", "export_png"]
133+
has_save_info = any(pattern in content for pattern in save_patterns)
134+
assert has_save_info, f"Missing save/output section in {filename}"
126135

127136

128137
class TestNoPlaceholders:

0 commit comments

Comments
 (0)