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
239 changes: 146 additions & 93 deletions scripts/radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,10 @@ def render(config: dict[str, Any]) -> None:
issues,
generated_at,
)
render_sitemap(sorted(feed_languages), generated_at, page_slugs)
topic_page_slugs = render_topic_pages(
{slug: config["topics"][slug]["title"] for slug in feed_topic_slugs}, repositories, issues, generated_at
)
render_sitemap(sorted(feed_languages), generated_at, page_slugs, topic_page_slugs)
log(f"rendered {len(language_rows)} language pages and {len(topic_rows)} topic pages")


Expand All @@ -1162,109 +1165,158 @@ def render_language_pages(
carry a real title, description, Open Graph tags and the newest beginner issues as plain
links. Returns the slugs written, for the sitemap.
"""
updated = generated_at[:16].replace("T", " ")
written = []
for language, slug in sorted(languages.items()):
items = [
issue
for issue in issues
if issue["level"] == "beginner" and repositories[issue["repo"]]["language"] == language
]
if not items:
continue
items.sort(key=lambda issue: issue["created"], reverse=True)
projects = len({issue["repo"] for issue in items})
url = f"{SITE_URL}beginner/{slug}/"
live = f"{SITE_URL}?{urllib.parse.urlencode({'language': language, 'level': 'beginner'})}"
name = html.escape(language)
title = f"Beginner {name} issues for your first open source contribution"
description = html.escape(
f"{len(items)} open, unclaimed good first issues in {language} from {projects} active projects. "
"Updated twice a day.",
quote=True,
)
rows = [
f' <li><a href="{html.escape(issue["url"], quote=True)}">{html.escape(issue["title"])}</a> '
f'<span class="muted">{html.escape(issue["repo"])}, opened {issue["created"]}</span></li>'
for issue in items[:LANGUAGE_PAGE_LIMIT]
]
structured = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": f"Beginner {language} issues",
"url": url,
"dateModified": generated_at,
"isPartOf": {"@type": "WebSite", "name": "Open Source Radar", "url": SITE_URL},
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Open Source Radar", "item": SITE_URL},
{"@type": "ListItem", "position": 2, "name": f"Beginner {language} issues", "item": url},
],
},
"mainEntity": {
"@type": "ItemList",
"numberOfItems": len(items),
"itemListElement": [
{"@type": "ListItem", "position": n, "url": issue["url"], "name": issue["title"]}
for n, issue in enumerate(items[:LANGUAGE_PAGE_LIMIT], 1)
],
},
}
# "</" inside a title must not close the script element early.
json_ld = json.dumps(structured, ensure_ascii=False).replace("</", "<\\/")
page = [
"<!doctype html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
f" <title>{title} | Open Source Radar</title>",
f' <meta name="description" content="{description}">',
f' <link rel="canonical" href="{url}">',
f' <meta property="og:title" content="{title}">',
f' <meta property="og:description" content="{description}">',
' <meta property="og:type" content="website">',
f' <meta property="og:url" content="{url}">',
f' <meta property="og:image" content="{SITE_URL}assets/og.png">',
' <meta name="twitter:card" content="summary_large_image">',
f' <link rel="alternate" type="application/rss+xml" title="{name} beginner issues" '
f'href="../../feeds/{slug}.xml">',
' <link rel="stylesheet" href="../../assets/style.css">',
f' <script type="application/ld+json">{json_ld}</script>',
" <script>",
' try { const t = localStorage.getItem("radar-theme"); '
"if (t) document.documentElement.dataset.theme = t; } catch (e) {}",
" </script>",
"</head>",
"<body>",
' <main class="wrap">',
' <section class="hero">',
f" <h1>Beginner {name} issues</h1>",
f' <p class="lede">{len(items)} open, unclaimed issues labeled for newcomers in {projects} '
f"active {name} projects. Updated {updated} UTC.</p>",
f' <p><a href="{html.escape(live, quote=True)}">Filter these on the live site</a> · '
f'<a href="../../feeds/{slug}.xml">RSS feed</a> · '
'<a href="https://github.com/TanbirRamim/open-source-radar/tree/main/guide">'
"First contribution guide</a></p>",
" </section>",
" <ul>",
*rows,
" </ul>",
' <p><a href="../../">All languages</a></p>',
" </main>",
"</body>",
"</html>",
"",
if _write_beginner_page(
f"beginner/{slug}/",
language,
items,
{"language": language, "level": "beginner"},
f"{slug}.xml",
generated_at,
):
written.append(slug)
return written


def render_topic_pages(
topics: dict[str, str],
repositories: dict[str, Any],
issues: list[dict[str, Any]],
generated_at: str,
) -> list[str]:
"""Write site/beginner/topic/<slug>/index.html, the topic twin of render_language_pages.

``topics`` maps a configured topic slug to its title. Returns the slugs written.
"""
written = []
for slug, title in sorted(topics.items()):
items = [
issue for issue in issues if issue["level"] == "beginner" and slug in repositories[issue["repo"]]["buckets"]
]
path = ROOT / "site" / "beginner" / slug / "index.html"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(page), encoding="utf-8")
written.append(slug)
if _write_beginner_page(
f"beginner/topic/{slug}/",
title,
items,
{"topic": slug, "level": "beginner"},
f"topics/{slug}.xml",
generated_at,
):
written.append(slug)
return written


def render_sitemap(languages: list[str], generated_at: str, page_slugs: Iterable[str] = ()) -> None:
def _write_beginner_page(
rel: str,
label: str,
items: list[dict[str, Any]],
live_params: dict[str, str],
feed: str,
generated_at: str,
) -> bool:
"""Write one static beginner page at site/<rel>index.html; False (and no file) if ``items`` is empty."""
if not items:
return False
items = sorted(items, key=lambda issue: issue["created"], reverse=True)
updated = generated_at[:16].replace("T", " ")
up = "../" * rel.count("/")
projects = len({issue["repo"] for issue in items})
url = f"{SITE_URL}{rel}"
live = f"{SITE_URL}?{urllib.parse.urlencode(live_params)}"
name = html.escape(label)
title = f"Beginner {name} issues for your first open source contribution"
description = html.escape(
f"{len(items)} open, unclaimed good first issues in {label} from {projects} active projects. "
"Updated twice a day.",
quote=True,
)
rows = [
f' <li><a href="{html.escape(issue["url"], quote=True)}">{html.escape(issue["title"])}</a> '
f'<span class="muted">{html.escape(issue["repo"])}, opened {issue["created"]}</span></li>'
for issue in items[:LANGUAGE_PAGE_LIMIT]
]
structured = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": f"Beginner {label} issues",
"url": url,
"dateModified": generated_at,
"isPartOf": {"@type": "WebSite", "name": "Open Source Radar", "url": SITE_URL},
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Open Source Radar", "item": SITE_URL},
{"@type": "ListItem", "position": 2, "name": f"Beginner {label} issues", "item": url},
],
},
"mainEntity": {
"@type": "ItemList",
"numberOfItems": len(items),
"itemListElement": [
{"@type": "ListItem", "position": n, "url": issue["url"], "name": issue["title"]}
for n, issue in enumerate(items[:LANGUAGE_PAGE_LIMIT], 1)
],
},
}
# "</" inside a title must not close the script element early.
json_ld = json.dumps(structured, ensure_ascii=False).replace("</", "<\\/")
page = [
"<!doctype html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
f" <title>{title} | Open Source Radar</title>",
f' <meta name="description" content="{description}">',
f' <link rel="canonical" href="{url}">',
f' <meta property="og:title" content="{title}">',
f' <meta property="og:description" content="{description}">',
' <meta property="og:type" content="website">',
f' <meta property="og:url" content="{url}">',
f' <meta property="og:image" content="{SITE_URL}assets/og.png">',
' <meta name="twitter:card" content="summary_large_image">',
f' <link rel="alternate" type="application/rss+xml" title="{name} beginner issues" href="{up}feeds/{feed}">',
f' <link rel="stylesheet" href="{up}assets/style.css">',
f' <script type="application/ld+json">{json_ld}</script>',
" <script>",
' try { const t = localStorage.getItem("radar-theme"); '
"if (t) document.documentElement.dataset.theme = t; } catch (e) {}",
" </script>",
"</head>",
"<body>",
' <main class="wrap">',
' <section class="hero">',
f" <h1>Beginner {name} issues</h1>",
f' <p class="lede">{len(items)} open, unclaimed issues labeled for newcomers in {projects} '
f"active {name} projects. Updated {updated} UTC.</p>",
f' <p><a href="{html.escape(live, quote=True)}">Filter these on the live site</a> · '
f'<a href="{up}feeds/{feed}">RSS feed</a> · '
'<a href="https://github.com/TanbirRamim/open-source-radar/tree/main/guide">'
"First contribution guide</a></p>",
" </section>",
" <ul>",
*rows,
" </ul>",
f' <p><a href="{up}">All languages and topics</a></p>',
" </main>",
"</body>",
"</html>",
"",
]
path = ROOT / "site" / rel / "index.html"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(page), encoding="utf-8")
return True


def render_sitemap(
languages: list[str], generated_at: str, page_slugs: Iterable[str] = (), topic_slugs: Iterable[str] = ()
) -> None:
"""Write site/sitemap.xml: the home page, the feed index and one beginner search per language.

Only languages that currently have beginner issues are listed, so search engines never
Expand All @@ -1273,6 +1325,7 @@ def render_sitemap(languages: list[str], generated_at: str, page_slugs: Iterable
lastmod = generated_at[:10]
urls = [SITE_URL, f"{SITE_URL}feeds/"]
urls += [f"{SITE_URL}beginner/{slug}/" for slug in page_slugs]
urls += [f"{SITE_URL}beginner/topic/{slug}/" for slug in topic_slugs]
urls += [f"{SITE_URL}?{urllib.parse.urlencode({'language': name, 'level': 'beginner'})}" for name in languages]
urlset = ET.Element("urlset", {"xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9"})
for loc in urls:
Expand Down
39 changes: 39 additions & 0 deletions scripts/test_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,45 @@ def test_lists_home_feeds_and_language_searches(self):
self.assertEqual({el.text for el in root.findall("s:url/s:lastmod", ns)}, {"2026-11-06"})


class TopicPageTests(unittest.TestCase):
def test_writes_topic_page_with_topic_links_and_sitemap_entry(self):
import tempfile

repositories = {"a/b": {"language": "Go", "buckets": ["web"]}, "c/d": {"language": "Go", "buckets": ["ai-ml"]}}
issues = [
{
"repo": "a/b",
"level": "beginner",
"created": "2026-11-01",
"title": "Fix form",
"url": "https://github.com/a/b/issues/1",
},
{
"repo": "c/d",
"level": "help-wanted",
"created": "2026-11-02",
"title": "Hard one",
"url": "https://github.com/c/d/issues/2",
},
]
with tempfile.TemporaryDirectory() as tmp, patch.object(radar, "ROOT", Path(tmp)):
slugs = radar.render_topic_pages(
{"web": "Web", "ai-ml": "AI & ML"}, repositories, issues, "2026-11-08T10:00:00Z"
)
page = (Path(tmp) / "site" / "beginner" / "topic" / "web" / "index.html").read_text(encoding="utf-8")
self.assertFalse((Path(tmp) / "site" / "beginner" / "topic" / "ai-ml").exists())
radar.render_sitemap([], "2026-11-08T10:00:00Z", (), slugs)
sitemap = (Path(tmp) / "site" / "sitemap.xml").read_text(encoding="utf-8")
self.assertEqual(slugs, ["web"])
self.assertIn("<h1>Beginner Web issues</h1>", page)
self.assertIn('href="https://tanbirramim.github.io/open-source-radar/beginner/topic/web/"', page)
self.assertIn('href="../../../feeds/topics/web.xml"', page)
self.assertIn('href="../../../assets/style.css"', page)
self.assertIn("?topic=web&amp;level=beginner", page)
self.assertNotIn("Hard one", page)
self.assertIn("<loc>https://tanbirramim.github.io/open-source-radar/beginner/topic/web/</loc>", sitemap)


class LanguagePageTests(unittest.TestCase):
def test_writes_crawlable_page_and_sitemap_entry(self):
import tempfile
Expand Down
Loading