mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: build the site with Astro Starlight
generate_site.py writes the Starlight project in website/ and nothing else: mkdocs.yml, the MkDocs hooks, theme overrides and stylesheet go, and the wiki is written in Starlight's syntax. Both workflows set up Node with the official actions/setup-node pinned by commit, run npm ci from the committed lockfile, build with the strict link validator and check the result with validate_site.py. The search weighs a page by what it answers: Start here pages first, a system page's cards and file names before a guide citing them, deep guides, launchers, aliases and frozen copies after. Status badges, hash blocks and the export change lists leave the index. The ranking sits at the centre of a range where the 13 queries of search_gate.py all answer, in the search box itself. 839 pages, every URL and 45,611 ids match the MkDocs build. The tests read the Starlight content and syntax.
This commit is contained in:
1 parent
abe240073c
commit
123d698e43
64 files changed
+742
-3118
No files matched your search
@@ -66,12 +66,21 @@ export default defineConfig({
|
||||
expressiveCode: { defaultProps: { wrap: true } },
|
||||
lastUpdated: false,
|
||||
pagination: true,
|
||||
// Measured on the ten queries of tools/search_gate.py, inside a range
|
||||
// where all ten answer: Starlight's defaults (0.1, 0.1, 2, 9) let a guide
|
||||
// that cites scph5501 once outrank the file's own card. A long page
|
||||
// is penalised a little more, and how often a word appears counts.
|
||||
// Measured on the queries of tools/search_gate.py, at the centre of a
|
||||
// range where all of them answer. How often a word appears counts more
|
||||
// than in Starlight's defaults (0.1, 0.1, 2, 9), a page's title weighs
|
||||
// ten times its text, so "dolphin" finds Dolphin, and the file names of
|
||||
// a system page (its `files` field, written by sitestarlight.py) count
|
||||
// half, so "scph5501" finds the Sony page without a file name winning
|
||||
// every other query.
|
||||
pagefind: {
|
||||
ranking: { pageLength: 0.3, termFrequency: 0.5, termSaturation: 1.4, termSimilarity: 9 },
|
||||
ranking: {
|
||||
pageLength: 0.1,
|
||||
termFrequency: 0.5,
|
||||
termSaturation: 1.4,
|
||||
termSimilarity: 9,
|
||||
metaWeights: { title: 10, files: 0.5 },
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
starlightSidebarTopics(sidebar.topics, sidebar.options),
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
---
|
||||
// Search ranking: Pagefind multiplies the score of words inside a weighted
|
||||
// element, so a Start here page outranks the reference page that names it.
|
||||
// Search weight: Pagefind multiplies the score of words inside a weighted
|
||||
// element, so a Start here page outranks the reference page that names it,
|
||||
// and an emulator outranks its launcher or its frozen copy.
|
||||
import Default from '@astrojs/starlight/components/MarkdownContent.astro';
|
||||
|
||||
const { boost } = Astro.locals.starlightRoute.entry.data;
|
||||
const { searchWeight } = Astro.locals.starlightRoute.entry.data;
|
||||
---
|
||||
|
||||
{
|
||||
boost ? (
|
||||
<div data-pagefind-weight={String(Math.min(10, boost * 2))}>
|
||||
searchWeight ? (
|
||||
<div data-pagefind-weight={String(searchWeight)}>
|
||||
<Default><slot /></Default>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -12,8 +12,8 @@ export const collections = {
|
||||
// Who the page is written for, from someone who knows nothing
|
||||
// (start) to someone adding to the project (contribute).
|
||||
level: z.enum(['start', 'setup', 'explore', 'reference', 'contribute']).optional(),
|
||||
// Search ranking of the first two levels.
|
||||
boost: z.number().min(1).max(10).optional(),
|
||||
// How much the site search weighs the page's words (1 by default).
|
||||
searchWeight: z.number().min(0.1).max(10).optional(),
|
||||
// The date the page's content carries, for the sitemap.
|
||||
lastmod: z.string().optional(),
|
||||
}),
|
||||
|
||||
@@ -367,7 +367,8 @@
|
||||
|
||||
/* Collapsible blocks keep the callout colour of their role */
|
||||
.sl-markdown-content details.rb-details {
|
||||
border-inline-start: 4px solid var(--sl-color-blue);
|
||||
border: 1px solid var(--sl-color-blue);
|
||||
border-radius: 4px;
|
||||
background: var(--sl-color-blue-low);
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The search gate: ten queries a visitor types, and the pages that answer them.
|
||||
"""The search gate: queries a visitor types, and the pages that answer them.
|
||||
|
||||
Runs measure.mjs against a served build and counts a query as answered when
|
||||
one of its expected pages is in the first three results of the site's own
|
||||
search box.
|
||||
Runs measure.mjs against a served build, through the site's own search box,
|
||||
and checks each query's expected pages against the rank it must reach: the
|
||||
first result, or the first three.
|
||||
|
||||
python website/tools/search_gate.py http://127.0.0.1:4321/retrobios/
|
||||
"""
|
||||
@@ -17,21 +17,24 @@ from pathlib import Path
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
|
||||
# Query, then the pages that answer it (path prefixes under the site).
|
||||
QUERIES: dict[str, tuple[str, ...]] = {
|
||||
"playstation bios": ("systems/sony/",),
|
||||
# The file itself: its card on the system page, or a core that reads it.
|
||||
"scph5501": ("systems/sony/", "emulators/beetle_psx/", "emulators/duckstation/",
|
||||
"emulators/duckstation-legacy/", "emulators/pcsx1/", "emulators/swanstation/",
|
||||
"emulators/ares/", "emulators/pcsx_rearmed/", "files/"),
|
||||
"batocera folder": ("platforms/batocera/",),
|
||||
"neogeo.zip": ("emulators/fbneo", "emulators/mame", "systems/snk/", "systems/arcade"),
|
||||
"duckstation": ("emulators/duckstation/", "emulators/duckstation-legacy/"),
|
||||
"steam deck": ("platforms/emudeck/", "platforms/retrodeck/", "which-pack/"),
|
||||
"missing bios": ("wiki/troubleshooting/", "wiki/what-are-bios-files/", "wiki/faq/"),
|
||||
"md5": ("wiki/verification-modes/",),
|
||||
"retroarch system folder": ("platforms/retroarch/",),
|
||||
"knulli": ("platforms/knulli/",),
|
||||
# Query: (pages that answer it, as path prefixes under the site, and the rank one
|
||||
# of them must reach). An emulator name must find the emulator itself first,
|
||||
# not its launcher or a frozen copy.
|
||||
QUERIES: dict[str, tuple[tuple[str, ...], int]] = {
|
||||
"batocera folder": (("platforms/batocera/",), 1),
|
||||
"missing bios": (("wiki/troubleshooting/", "wiki/what-are-bios-files/", "wiki/faq/"), 1),
|
||||
"neogeo.zip": (("emulators/fbneo", "emulators/mame", "systems/snk/", "systems/arcade"), 1),
|
||||
"retroarch system folder": (("platforms/retroarch/",), 1),
|
||||
"installer": (("wiki/installer/", "wiki/first-install/"), 1),
|
||||
# The file's card on its system page, where it is downloaded.
|
||||
"scph5501": (("systems/sony/",), 3),
|
||||
"md5": (("wiki/verification-modes/",), 1),
|
||||
"where do bios files go": (("which-pack/", "wiki/first-install/"), 1),
|
||||
"dolphin": (("emulators/dolphin/",), 1),
|
||||
"duckstation": (("emulators/duckstation/",), 1),
|
||||
"playstation bios": (("systems/sony/",), 3),
|
||||
"steam deck": (("platforms/emudeck/", "platforms/retrodeck/", "which-pack/"), 1),
|
||||
"knulli": (("platforms/knulli/",), 1),
|
||||
}
|
||||
|
||||
|
||||
@@ -48,12 +51,14 @@ def main() -> int:
|
||||
if "query" not in result:
|
||||
print(f"first search: {result['first_search_kb']} KB in {result['first_search_requests']} requests")
|
||||
continue
|
||||
top = [entry.split(" | ")[1][len(root):] for entry in result["top"][:3]]
|
||||
hit = any(page.startswith(QUERIES[result["query"]]) for page in top)
|
||||
pages, rank = QUERIES[result["query"]]
|
||||
top = [entry.split(" | ") for entry in result["top"][:3]]
|
||||
hit = any(href[len(root):].startswith(pages) for _title, href in top[:rank])
|
||||
answered += hit
|
||||
print(f"{'ok ' if hit else 'MISS'} {result['query']!r}: {', '.join(top)}")
|
||||
print(f"{answered}/{len(QUERIES)} queries answered in the top 3")
|
||||
return 0 if answered >= 9 else 1
|
||||
titles = "; ".join(title for title, _href in top)
|
||||
print(f"{'ok ' if hit else 'MISS'} {result['query']!r} (top {rank}): {titles}")
|
||||
print(f"{answered}/{len(QUERIES)} queries answered")
|
||||
return 0 if answered == len(QUERIES) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,18 +6,18 @@ const [base, query, ranking] = process.argv.slice(2);
|
||||
const browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome' });
|
||||
const page = await browser.newPage();
|
||||
await page.goto(base, { waitUntil: 'networkidle' });
|
||||
const rows = await page.evaluate(async ({ root, query, ranking }) => {
|
||||
const rows = await page.evaluate(async ({ root, query, ranking, top }) => {
|
||||
const pagefind = await import(`${root}pagefind/pagefind.js`);
|
||||
if (ranking) await pagefind.options({ ranking: JSON.parse(ranking) });
|
||||
const search = await pagefind.search(query);
|
||||
const top = search.results.slice(0, 8);
|
||||
const data = await Promise.all(top.map((result) => result.data()));
|
||||
return top.map((result, index) => ({
|
||||
const shown = search.results.slice(0, top);
|
||||
const data = await Promise.all(shown.map((result) => result.data()));
|
||||
return shown.map((result, index) => ({
|
||||
score: result.score.toFixed(3),
|
||||
url: data[index].url,
|
||||
words: data[index].word_count,
|
||||
hits: result.words.length,
|
||||
}));
|
||||
}, { root: base, query, ranking });
|
||||
}, { root: base, query, ranking, top: Number(process.env.TOP || 8) });
|
||||
for (const row of rows) console.log(row.score, row.words, row.hits, row.url);
|
||||
await browser.close();
|
||||
Reference in new issue
Block a user