diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 3c53f0aee..851cefaf6 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -772,6 +772,11 @@ def generate_get_page(platform_name: str) -> str: pack = release_record.pack_for(platform_name, record) released = has_release_pack(platform_name) title = get_page_title(platform_name, display) + # The page changes with the release it lists: its tag is its date, the + # one the sitemap gives (vYYYY.MM.DD). + tag_date = re.fullmatch(r"v(\d{4})\.(\d{2})\.(\d{2})", str(record.get("tag", ""))) + if released and pack and tag_date: + PAGE_DATA[f"{release_record.GET_DIR}/{platform_name}.md"] = {"dateModified": "-".join(tag_date.groups())} if released and pack and pack.get("assets"): description = (f"The BIOS pack for {display}: {_fmt_size(pack['download_size'])} to " "download. The address stays the same and always gives the latest ZIP.") @@ -3014,6 +3019,11 @@ def record_emulator_page(name: str, profile: dict) -> None: fetched = [f for f in listed if f.get("name") and not comes_with_app(f)] systems = profile.get("systems") or [] first = systems[0] if systems else "" + makers = [_system_page_map[sys_id][0] for sys_id in systems if sys_id in _system_page_map] + if makers: + sitestarlight.EMULATOR_CARDS[f"emulators/{name}"] = makers[0] + else: + sitestarlight.EMULATOR_CARDS.pop(f"emulators/{name}", None) # The first system's files first: "dreamcast bios" found Flycast's # page, whose required NAOMI archives came before dc_boot.bin. names: dict[str, str] = {} @@ -3027,8 +3037,7 @@ def record_emulator_page(name: str, profile: dict) -> None: system_name(first) if 0 < len(systems) <= 5 else "", list(names.values()), len({Path(str(f["name"])).name.casefold() for f in fetched if f.get("required") is True}), - # Ship of Harkinian reads game ROMs: they are no BIOS. - "BIOS" if len(firmware) * 2 >= len(fetched) else "files", + _snippet_noun(names.values(), len(firmware), len(fetched)), ) if listed and all(comes_with_app(f) for f in listed): NOTHING_TO_FETCH.add(key) @@ -7240,6 +7249,9 @@ SYSTEM_PAGES_MOVED = { "systems/arcade-sharp-mz-2500.md": "systems/sharp.md", "systems/arcade-miuchiz.md": "systems/arcade.md", "systems/arcade-yamaha-ym2608.md": "systems/arcade.md", + # Oric is filed under its maker since 2026-10-23, and a reader still + # followed the old address to the 404 page. + "systems/oric.md": "systems/tangerine.md#oric", "systems/other-scummvm.md": "systems/scummvm.md", "systems/other-sdlpal.md": "systems/sdlpal.md", "systems/sega-dreamcast.md": "systems/sega.md", @@ -7895,6 +7907,19 @@ def generate_emulators_index(profiles: dict) -> str: return "\n".join(lines) + "\n" +def _snippet_noun(names, firmware: int, fetched: int) -> str: + """What an emulator's files are called in its snippet. A Switch + emulator reads keys, and "eden keys" is what readers search for; Ship + of Harkinian reads game ROMs, which are no BIOS.""" + names = list(names) + keys = sum("key" in name.casefold() for name in names) + if keys >= 2 and any("firmware" in name.casefold() for name in names): + return "keys and firmware" + if names and keys * 2 > len(names): + return "keys" + return "BIOS" if firmware * 2 >= fetched else "files" + + def comes_with_app(f: dict) -> bool: """The emulator installs the file in the build the entry describes. diff --git a/scripts/site_insights.py b/scripts/site_insights.py index ec6949f66..f9edc0bfa 100644 --- a/scripts/site_insights.py +++ b/scripts/site_insights.py @@ -139,8 +139,14 @@ def fetch_goatcounter(start: dt.date, end: dt.date) -> dict[str, list[tuple[str, if not answer.get("more"): break refs = _get_json(f"{GOATCOUNTER}/stats/toprefs?{urllib.parse.urlencode({**span, 'limit': 50})}", auth) - referrers = [(r.get("name") or "(direct)", int(r["count"])) for r in refs.get("stats", [])] - return {"hits": hits, "referrers": referrers} + total = _get_json(f"{GOATCOUNTER}/stats/total?{urllib.parse.urlencode(span)}", auth) + # An action carries no referrer, and GoatCounter files it as a direct + # visit: 592 of the first day's 1,145 "direct" were clicks. + events = int(total.get("total_events", 0)) + referrers = [("(direct)", max(int(r["count"]) - events, 0)) if not r.get("name") else (r["name"], int(r["count"])) + for r in refs.get("stats", [])] + return {"hits": hits, "referrers": sorted(referrers, key=lambda row: -row[1]), + "visits": int(total.get("total", 0)) - events, "actions": events} def fetch_github() -> dict: @@ -246,7 +252,8 @@ def report(start: dt.date, end: dt.date, queries: list[Query] | None, goat: dict "helpful": "Did this page help?", "get": "Pack pages opened", "download": "Release downloads", "file": "File downloads", "copy": "Copy buttons", "out": "Links leaving the site", "pages": "Pages"} - out += ["## On the site", ""] + out += ["## On the site", "", + f"{goat.get('visits', 0):,} page views, {goat.get('actions', 0):,} actions.", ""] for kind, title in named.items(): if kind in actions: out += [f"### {title}", "", *_table(("Name", "Visits"), [(n, f"{c:,}") for n, c in actions[kind]])] diff --git a/scripts/sitestarlight.py b/scripts/sitestarlight.py index f4ffbf027..4f39e20ce 100644 --- a/scripts/sitestarlight.py +++ b/scripts/sitestarlight.py @@ -328,12 +328,22 @@ def og_section(rel: str, level: str) -> str: OG_DIR = Path(__file__).resolve().parent.parent / "docs_assets" / "og" +# An emulator page's card is that of the maker of the first system it runs, +# by page path: "emulators/mgba.mdx" -> "nintendo". One card per emulator +# would be some 900 images in the repository; the maker's names the +# consoles. Filled by generate_site.record_emulator_page. +EMULATOR_CARDS: dict[str, str] = {} + + def og_image(rel: str, level: str) -> str: - """The sharing card's address: the page's own when it has one, else - its section's.""" + """The sharing card's address: the page's own when it has one, its + maker's for an emulator, else its section's.""" own = re.fullmatch(r"(platforms|systems)/([a-z0-9_-]+)\.mdx?", rel) if own and own[2] != "index" and (OG_DIR / own[1] / f"{own[2]}.png").is_file(): return f"{SITE_URL}assets/og/{own[1]}/{own[2]}.png" + maker = EMULATOR_CARDS.get(re.sub(r"\.mdx?$", "", rel)) + if maker and (OG_DIR / "systems" / f"{maker}.png").is_file(): + return f"{SITE_URL}assets/og/systems/{maker}.png" return f"{SITE_URL}assets/og/{og_section(rel, level)}.png" @@ -350,6 +360,11 @@ def page_head(rel: str, level: str, title: str, browser_title: str, structured: if rel == NOT_FOUND: return head + [{"tag": "meta", "attrs": {"name": "robots", "content": "noindex"}}] image = og_image(rel, level) + if browser_title != title: + # Starlight writes og:title from the H1 ("mGBA"): a shared link + # reads as the search result does ("mGBA BIOS files"). + head += [{"tag": "meta", "attrs": {"property": "og:title", "content": browser_title}}, + {"tag": "meta", "attrs": {"name": "twitter:title", "content": browser_title}}] head += [ {"tag": "meta", "attrs": {"property": "og:image", "content": image}}, {"tag": "meta", "attrs": {"property": "og:image:width", "content": "1200"}}, diff --git a/tests/test_site_nav.py b/tests/test_site_nav.py index 934c97fd4..61de56a67 100644 --- a/tests/test_site_nav.py +++ b/tests/test_site_nav.py @@ -332,6 +332,28 @@ class PlatformShareCards(unittest.TestCase): self.assertEqual(sitestarlight.og_image("systems/no-such-maker.md", "explore"), f"{sitestarlight.SITE_URL}assets/og/explore.png") + def test_an_emulator_page_shares_its_makers_card(self): + """Every emulator page shared the generic Explore card.""" + saved = dict(sitestarlight.EMULATOR_CARDS) + self.addCleanup(lambda: (sitestarlight.EMULATOR_CARDS.clear(), + sitestarlight.EMULATOR_CARDS.update(saved))) + sitestarlight.EMULATOR_CARDS.update({"emulators/mgba": "nintendo", "emulators/odd": "no-such-maker"}) + self.assertEqual(sitestarlight.og_image("emulators/mgba.mdx", "explore"), + f"{sitestarlight.SITE_URL}assets/og/systems/nintendo.png") + self.assertEqual(sitestarlight.og_image("emulators/odd.md", "explore"), + f"{sitestarlight.SITE_URL}assets/og/explore.png") + + def test_a_shared_link_reads_the_search_title(self): + """og:title was the H1, "mGBA", under a result titled "mGBA BIOS files".""" + head = sitestarlight.page_head("emulators/mgba.md", "explore", "mGBA", "mGBA BIOS files", {}) + shared = {tag["attrs"].get("property") or tag["attrs"].get("name"): tag["attrs"]["content"] + for tag in head if tag.get("attrs", {}).get("content") and tag["tag"] == "meta"} + self.assertEqual(shared["og:title"], "mGBA BIOS files") + self.assertEqual(shared["twitter:title"], "mGBA BIOS files") + plain = sitestarlight.page_head("wiki/faq.md", "start", "FAQ", "FAQ", {}) + self.assertFalse(any(tag.get("attrs", {}).get("property") == "og:title" for tag in plain)) + + # The built site diff --git a/tests/test_site_search.py b/tests/test_site_search.py index 242466820..c5b831f2e 100644 --- a/tests/test_site_search.py +++ b/tests/test_site_search.py @@ -264,6 +264,13 @@ class Descriptions(unittest.TestCase): self.assertLessEqual(len(text), 155) self.assertFalse(text.endswith("..."), text) + def test_a_switch_emulator_reads_keys(self) -> None: + self.assertEqual(gs._snippet_noun(["prod.keys", "dev.keys", "title.keys", "key_retail.bin"], 4, 4), "keys") + self.assertEqual(gs._snippet_noun(["prod.keys", "title.keys", ".zip", "a.txt"], 3, 4), + "keys and firmware") + self.assertEqual(gs._snippet_noun(["gba_bios.bin", "gb_bios.bin"], 2, 2), "BIOS") + self.assertEqual(gs._snippet_noun(["oot.z64", "oot-mq.z64", "font.bin"], 1, 3), "files") + def test_an_any_name_snippet_says_what_to_do(self) -> None: saved = dict(gs.PAGE_DATA) self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved))) @@ -357,6 +364,15 @@ class GetPagesSitInYourSetup(unittest.TestCase): with self.subTest(page=rel): self.assertIn("/" + sitestarlight._sidebar_link(rel).strip("/"), setup) + def test_a_get_page_is_dated_by_its_release(self): + """The sitemap gave no date to the pages that change only with a release.""" + saved = dict(gs.PAGE_DATA) + self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved))) + record = json.loads((REPO_ROOT / "release.json").read_text(encoding="utf-8")) + tag = re.fullmatch(r"v(\d{4})\.(\d{2})\.(\d{2})", record["tag"]) + gs.generate_get_page("retroarch") + self.assertEqual(gs.PAGE_DATA["get/retroarch.md"], {"dateModified": "-".join(tag.groups())}) + def test_no_page_is_left_out_of_every_tab(self): nav = _nav() present = _present(nav) | {"orphans/alone.md"} diff --git a/tests/test_site_systems.py b/tests/test_site_systems.py index 726c6371b..f725d5c96 100644 --- a/tests/test_site_systems.py +++ b/tests/test_site_systems.py @@ -1189,7 +1189,8 @@ class EnginesGoByEngine(unittest.TestCase): def test_moved_pages_redirect(self) -> None: if not SYSTEMS.is_dir(): self.skipTest("pages not generated") - for old, new in gs.SYSTEM_PAGES_MOVED.items(): + for old, target in gs.SYSTEM_PAGES_MOVED.items(): + new = target.partition("#")[0] self.assertFalse((CONTENT / old).exists() or (CONTENT / old).with_suffix(".mdx").exists(), old) # A page with tabs is written as MDX. self.assertTrue((CONTENT / new).exists() or (CONTENT / new).with_suffix(".mdx").exists(), new) @@ -1197,6 +1198,7 @@ class EnginesGoByEngine(unittest.TestCase): if redirects.is_file(): table = json.loads(redirects.read_text(encoding="utf-8")) self.assertEqual(table.get("/systems/other-scummvm/"), "/systems/scummvm/") + self.assertEqual(table.get("/systems/oric/"), "/systems/tangerine/#oric") class OneLabelOnceOnABoardPage(unittest.TestCase):