Skip to content

Commit 3c74c99

Browse files
authored
Merge pull request #12 from LeagueToolkit/class-canonical-redirects
feat: route class hash names to the actual page
2 parents 19645b9 + c5baebb commit 3c74c99

1,000 files changed

Lines changed: 5017 additions & 4809 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/generate-db.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* outDir/index.json (fetched client-side)
1111
* outDir/classIndex.json (fetched client-side)
1212
* outDir/classSidebar.json (fetched client-side, grouped sidebar view)
13+
* outDir/classHashes.json (fetched client-side by the 404 resolver)
1314
* mdxDir/<ClassName>.mdx (Starlight docs)
1415
*
1516
* Usage:
@@ -40,6 +41,7 @@ import type {
4041
ChangelogCounts,
4142
ChangelogPatch,
4243
ClassDocumentation,
44+
ClassHashIndex,
4345
ClassJson,
4446
ClassSidebar,
4547
ClassSidebarEntry,
@@ -60,6 +62,7 @@ import type { MetaDb, PropRevision } from "./meta-db";
6062
// attached to form the emitted ClassJson.
6163
type ClassDoc = {
6264
name: string; // resolved type name or raw hex
65+
hash: string; // canonical class hash (see canonHash)
6366
bases: string[]; // zero or more base names (resolved or hex)
6467
properties: Property[];
6568
since?: string; // patch the class was added in
@@ -109,6 +112,13 @@ function safeName(name: string) {
109112
function classSlug(name: string) {
110113
return safeName(name).toLowerCase();
111114
}
115+
// Canonical spelling of a hash: "0x" + 8 lowercase, zero-padded hex digits.
116+
// meta.db.json stores unpadded hex ("0x6516a"), so the same class is spelled
117+
// two ways across the project; the API canonicalizes the same way (see
118+
// api/scripts/lib/resolver.ts) and classHashes.json is keyed by this form.
119+
function canonHash(hash: string) {
120+
return "0x" + hash.slice(2).toLowerCase().padStart(8, "0");
121+
}
112122
// Heading anchor slug for a property, matching the ids rehype-slug assigns to
113123
// the "## <name>" headings generateMDX emits (github-slugger semantics:
114124
// lowercase, drop punctuation, spaces → hyphens). Property names are C++-style
@@ -200,6 +210,7 @@ function loadMetaDb(db: MetaDb): ClassDoc[] {
200210

201211
const doc: ClassDoc = {
202212
name: klass.name ?? khash,
213+
hash: canonHash(khash),
203214
bases: currentClass.bases.map(nameOf),
204215
properties: [],
205216
};
@@ -781,15 +792,30 @@ async function main() {
781792
// Emit classIndex.json for type auto-linking
782793
const classIndex: Record<string, string> = {};
783794
for (const c of classes) {
784-
const slug = safeName(c.name).toLowerCase();
785-
classIndex[c.name] = `/classes/${slug}`;
795+
classIndex[c.name] = `/classes/${classSlug(c.name)}`;
786796
}
787797
const classIndexPath = join(outDir, "classIndex.json");
788798
await writeIfChanged(
789799
classIndexPath,
790800
JSON.stringify(classIndex, null, pretty ? 2 : 0)
791801
);
792802

803+
// Emit classHashes.json - canonical hash → page slug for every class, the
804+
// lookup table behind the 404 resolver (components/NotFound.astro). A page
805+
// only exists under its display name, so every other spelling of the same
806+
// class (its hash once the name was resolved, a padded/unprefixed hash, the
807+
// name in the wrong case) 404s without it. Sorted by hash so the diff is
808+
// stable when a name resolves. Always minified regardless of --pretty: it's
809+
// only ever fetched by the browser; /v1/hashes is the readable form.
810+
const classHashes: ClassHashIndex = {};
811+
for (const c of [...classes].sort((a, b) => (a.hash < b.hash ? -1 : 1))) {
812+
classHashes[c.hash] = classSlug(c.name);
813+
}
814+
await writeIfChanged(
815+
join(outDir, "classHashes.json"),
816+
JSON.stringify(classHashes)
817+
);
818+
793819
// Emit symbols.json - the flat identifier index the search modal's symbol
794820
// search fetches (Search.astro + utils/symbolSearch.ts). Property names are
795821
// deduped; owners are indices into `classes`. Always minified regardless of

site/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,24 @@ Three components in `src/components/starlight/` replace Starlight's own, configu
8282
`astro.config.mjs`:
8383

8484
- **`ResizableSidebar.astro`** - adds the drag-to-resize handle, and renders the Classes group
85-
**client-side** from `/db/classIndex.json`. This is the important one: putting ~5,300 class links
85+
**client-side** from `/db/classSidebar.json`. This is the important one: putting ~5,300 class links
8686
into the static sidebar meant every HTML file carried them, at roughly 850 KB per page and a 4.3 GB
8787
`dist/`. Do not move that group back into the `sidebar` config in `astro.config.mjs`.
8888
- **`Search.astro`** - Starlight's search with `highlightParam` enabled, so results link with
8989
`?highlight=<term>` and the term is highlighted on arrival. The `/pagefind/` bundle only exists in
9090
production builds, so the highlight script is guarded and silently skipped in dev.
9191
- **`PageTitle.astro`** - class page titles.
9292

93+
## The 404 page
94+
95+
`src/content/docs/404.mdx` overrides Starlight's built-in 404 route, which is what GitHub Pages
96+
serves for every unknown path. Besides the recovery links, `components/NotFound.astro` rescues class
97+
lookups: a class page exists only under its display name, so `/classes/<x>` 404s whenever `<x>` is
98+
another spelling of the same class - its hash once the name has been resolved from the hash tables,
99+
a padded or unprefixed hash, or the name in the wrong case. The script canonicalizes the segment
100+
(`utils/classHash.ts`), looks it up in `/db/classHashes.json`, and redirects to the real page,
101+
carrying the query and anchor along. Anything it cannot resolve falls back to the plain 404 text.
102+
93103
## Conventions
94104

95105
Component architecture, prop typing, and styling rules are in [CLAUDE.md](../CLAUDE.md) at the

site/src/components/NotFound.astro

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
/**
3+
* Body of the 404 page (content/docs/404.mdx), which GitHub Pages serves for
4+
* every unknown path.
5+
*
6+
* Beyond the generic recovery links it rescues class lookups: a class page
7+
* exists only under its display name, so `/classes/<x>` 404s whenever `<x>`
8+
* is any other spelling of the same class - its hash once the name has been
9+
* resolved from the hash tables, a padded or unprefixed hash, or the name in
10+
* the wrong case. classHashes.json maps every canonical hash onto the slug
11+
* the page is really at, so those all resolve client-side and redirect.
12+
*/
13+
---
14+
15+
<div class="not-found">
16+
<p class="nf-status" data-nf-status hidden aria-live="polite"></p>
17+
18+
<div data-nf-generic>
19+
<p>
20+
That page doesn't exist. If you followed a link to a class, its name may
21+
have changed since - press <kbd>Ctrl</kbd> <kbd>K</kbd> to search every
22+
class and property on the site.
23+
</p>
24+
<p class="nf-links">
25+
<a href="/classes/">Class reference</a>
26+
<a href="/changelog/">Patch changelog</a>
27+
<a href="/">Home</a>
28+
</p>
29+
</div>
30+
</div>
31+
32+
<script>
33+
import type { ClassHashIndex } from "../types";
34+
import { parseClassHash } from "../utils/classHash";
35+
36+
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
37+
const status = document.querySelector<HTMLElement>("[data-nf-status]");
38+
const generic = document.querySelector<HTMLElement>("[data-nf-generic]");
39+
40+
/** "/classes/0x6516A/" → "0x6516A"; null when this isn't a class lookup. */
41+
function classSegment(): string | null {
42+
const path = location.pathname.startsWith(base)
43+
? location.pathname.slice(base.length)
44+
: location.pathname;
45+
// Case-insensitive: "/Classes/…" is a 404 too, and the redirect below
46+
// rebuilds the path from the lowercase form either way.
47+
const match = /^\/classes\/([^/]+)\/?$/i.exec(path);
48+
if (!match) return null;
49+
try {
50+
return decodeURIComponent(match[1]!);
51+
} catch {
52+
return match[1]!;
53+
}
54+
}
55+
56+
/** The slug this spelling of a class has a page at, if any. */
57+
async function resolveSlug(segment: string): Promise<string | null> {
58+
const res = await fetch(`${base}/db/classHashes.json`);
59+
if (!res.ok) return null;
60+
const hashes: ClassHashIndex = await res.json();
61+
// Right name or hash, wrong case: slugs are lowercase, and the index's
62+
// values are exactly the set of slugs a page exists at.
63+
const slugs = new Set(Object.values(hashes));
64+
const lower = segment.toLowerCase();
65+
if (slugs.has(lower)) return lower;
66+
// Otherwise a hash in some other spelling than the one the page is at.
67+
const hash = parseClassHash(segment);
68+
return (hash && hashes[hash]) || null;
69+
}
70+
71+
const segment = classSegment();
72+
if (segment && status && generic) {
73+
const label = document.createElement("code");
74+
label.textContent = segment;
75+
status.replaceChildren("Looking for ", label, "…");
76+
status.hidden = false;
77+
generic.hidden = true;
78+
79+
resolveSlug(segment)
80+
.then((slug) => {
81+
const target = slug ? `${base}/classes/${slug}/` : null;
82+
// Never redirect onto the path we are already on: if the index knows
83+
// a slug whose page is missing (a deploy caught mid-flight), bouncing
84+
// to it would land back here and loop.
85+
if (target && target !== location.pathname) {
86+
// replace(), not assign(): Back should return to wherever the stale
87+
// link was, not to this page. Query and hash ride along - they may
88+
// carry a ?highlight= term or a property anchor.
89+
location.replace(target + location.search + location.hash);
90+
return;
91+
}
92+
status.replaceChildren("No class matches ", label, ".");
93+
generic.hidden = false;
94+
})
95+
.catch(() => {
96+
status.hidden = true;
97+
generic.hidden = false;
98+
});
99+
}
100+
</script>
101+
102+
<style>
103+
.not-found {
104+
max-width: 45rem;
105+
margin: 0 auto;
106+
text-align: center;
107+
}
108+
109+
.nf-status {
110+
font-size: var(--sl-text-body);
111+
color: var(--sl-color-gray-2);
112+
}
113+
114+
.nf-status code {
115+
color: var(--sl-color-white);
116+
}
117+
118+
.nf-links {
119+
display: flex;
120+
flex-wrap: wrap;
121+
justify-content: center;
122+
gap: 0.5rem 1.5rem;
123+
}
124+
</style>

site/src/content/docs/404.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
# Overrides Starlight's built-in 404 route (it renders this entry when one
3+
# exists), which is what GitHub Pages serves for every unknown path.
4+
title: "404"
5+
description: The page you were looking for could not be found.
6+
template: splash
7+
editUrl: false
8+
pagefind: false
9+
hero:
10+
tagline: Page not found.
11+
---
12+
13+
import NotFound from '../../components/NotFound.astro';
14+
15+
<NotFound />

site/src/content/docs/changelog/13-17.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/13-17.7a62af4467e8.json" />
10+
<PatchChangelog file="/db/changelog/13-17.c0ea8f9d5df6.json" />

site/src/content/docs/changelog/13-18.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/13-18.cb03b7fe0b5e.json" />
10+
<PatchChangelog file="/db/changelog/13-18.8ba53335449d.json" />

site/src/content/docs/changelog/13-20.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/13-20.2c1343efa9ef.json" />
10+
<PatchChangelog file="/db/changelog/13-20.f3187eb457ed.json" />

site/src/content/docs/changelog/13-21.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/13-21.c00a5e5e51a4.json" />
10+
<PatchChangelog file="/db/changelog/13-21.4c1b8b35f0eb.json" />

site/src/content/docs/changelog/13-23.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/13-23.67b40661b6b3.json" />
10+
<PatchChangelog file="/db/changelog/13-23.8650aa6c5b8e.json" />

site/src/content/docs/changelog/14-1.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sidebar:
77

88
import PatchChangelog from '../../../components/PatchChangelog.astro';
99

10-
<PatchChangelog file="/db/changelog/14-1.d45b7fd1d46f.json" />
10+
<PatchChangelog file="/db/changelog/14-1.67f655bc7eae.json" />

0 commit comments

Comments
 (0)