Skip to content

Commit e100f84

Browse files
committed
Update sitemap
1 parent e5eb6f2 commit e100f84

File tree

7 files changed

+243
-54
lines changed

7 files changed

+243
-54
lines changed

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
<meta property="og:title" content="Source 2 Schema Explorer" />
99
<meta property="og:description" content="Browse and explore Valve Source 2 engine schemas, classes, enums, and types for Counter-Strike 2, Dota 2, and Deadlock." />
1010
<meta property="og:type" content="website" />
11+
<meta property="og:site_name" content="Source 2 Schema Explorer" />
12+
<meta property="og:url" content="https://s2v.app/SchemaExplorer/" />
1113
<meta property="og:image" content="/src/source2viewer.png" />
1214
<meta name="theme-color" content="#63a1ff" />
15+
<link rel="canonical" href="https://s2v.app/SchemaExplorer/" />
16+
<link rel="sitemap" href="/SchemaExplorer/sitemap.xml" />
1317
<script>
1418
var t = localStorage.getItem("theme");
1519
if (t === "dark" || (t !== "light" && matchMedia("(prefers-color-scheme: dark)").matches))
@@ -19,6 +23,9 @@
1923

2024
<body>
2125
<div id="root"></div>
26+
<noscript>
27+
<p>This app requires JavaScript. You can browse the full schema listing on the <a href="/SchemaExplorer/sitemap/">sitemap</a>.</p>
28+
</noscript>
2229
<script type="module" src="/src/index.tsx"></script>
2330
</body>
2431
</html>

scripts/generate-sitemap.ts

Lines changed: 121 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ function escapeHtml(s: string): string {
2727
.replace(/"/g, "&quot;");
2828
}
2929

30+
function toIsoDate(date: string, time?: string): string {
31+
const d = new Date(`${date} ${time ?? "00:00:00"} UTC`);
32+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
33+
}
34+
3035
function formatMetadata(metadata: SchemaMetadataEntry[] | undefined): string {
3136
if (!metadata || metadata.length === 0) return "";
3237
const parts = metadata.map((m) => (m.value !== undefined ? `${m.name}=${m.value}` : m.name));
@@ -42,25 +47,50 @@ function renderTable(headers: string[], rows: string[][]): string {
4247
return html;
4348
}
4449

45-
function htmlPage(title: string, description: string, body: string): string {
50+
interface HtmlPageOptions {
51+
isoDate?: string;
52+
canonicalUrl?: string;
53+
schemaType?: string;
54+
}
55+
56+
function htmlPage(
57+
title: string,
58+
description: string,
59+
body: string,
60+
options: HtmlPageOptions = {},
61+
): string {
62+
const { isoDate, canonicalUrl, schemaType = "WebPage" } = options;
63+
let head = "";
64+
if (isoDate) head += `\n<meta name="date" content="${isoDate}" itemprop="dateModified">`;
65+
if (canonicalUrl) {
66+
head += `\n<link rel="canonical" href="${canonicalUrl}">`;
67+
head += `\n<meta property="og:url" content="${canonicalUrl}">`;
68+
}
69+
head += `\n<link rel="sitemap" href="${basePath}/sitemap.xml">`;
70+
head += `\n<meta property="og:type" content="website">`;
71+
head += `\n<meta property="og:site_name" content="Source 2 Schema Explorer">`;
72+
head += `\n<meta property="og:title" content="${escapeHtml(title)}">`;
73+
head += `\n<meta property="og:description" content="${escapeHtml(description)}">`;
74+
4675
return `<!DOCTYPE html>
4776
<html lang="en">
4877
<head>
4978
<meta charset="utf-8">
5079
<meta name="viewport" content="width=device-width, initial-scale=1">
5180
<title>${escapeHtml(title)} - Sitemap - Source 2 Schema Explorer</title>
52-
<meta name="description" content="${escapeHtml(description)}">
53-
<meta name="color-scheme" content="light dark">
81+
<meta name="description" content="${escapeHtml(description)}" itemprop="description">
82+
<meta name="color-scheme" content="light dark">${head}
5483
<style>
55-
body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
84+
body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; overflow-wrap: break-word; word-break: break-all; }
85+
.note { position: sticky; top: 0; background: Canvas; margin: 0 -20px; padding: 8px 20px; border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent); z-index: 1; }
5686
dt { margin-top: 0.5em; }
5787
dd { margin-left: 1.5em; }
5888
table { border-collapse: collapse; width: 100%; margin: 4px 0 0 1.5em; }
5989
th, td { text-align: left; padding: 2px 8px; border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent); font-family: monospace; }
6090
th { font-weight: normal; opacity: 0.6; }
6191
</style>
6292
</head>
63-
<body>
93+
<body itemscope itemtype="https://schema.org/${schemaType}">
6494
${body}
6595
</body>
6696
</html>
@@ -74,12 +104,41 @@ try {
74104
}
75105

76106
let totalFiles = 0;
77-
const sitemapXmlUrls: string[] = [];
107+
const sitemapXmlUrls: { url: string; lastmod?: string }[] = [];
78108

79109
const appRoot = "..";
80110

81-
function banner(root: string): string {
82-
return `<p role="note"><strong>This is a static listing page for search engines.</strong> Click on a type name to open it in the <a href="${root}/">Schema Explorer</a> with full search, cross-references, and inheritance views.</p>`;
111+
function banner(root: string, revision?: number, versionDate?: string, isoDate?: string): string {
112+
let html = `<p class="note" role="note"><strong>This is a static listing page for search engines.</strong> Click on a type name to open it in the <a href="${root}/">Schema Explorer</a> with full search, cross-references, and inheritance views.`;
113+
if (revision && versionDate) {
114+
html += `<br><small>Revision ${revision} &middot; <time datetime="${isoDate}">${escapeHtml(versionDate)}</time></small>`;
115+
}
116+
html += `</p>`;
117+
return html;
118+
}
119+
120+
function breadcrumb(items: { name: string; href?: string }[]): string {
121+
const lis = items
122+
.map(
123+
(item, i) =>
124+
`<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">${
125+
item.href
126+
? `<a itemprop="item" href="${item.href}"><span itemprop="name">${escapeHtml(item.name)}</span></a>`
127+
: `<span itemprop="name">${escapeHtml(item.name)}</span>`
128+
}<meta itemprop="position" content="${i + 1}"></span>`,
129+
)
130+
.join(" &rsaquo; ");
131+
return `<nav aria-label="Breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">${lis}</nav>`;
132+
}
133+
134+
function moduleNav(moduleFiles: { mod: string; fileName: string }[], currentMod: string): string {
135+
if (moduleFiles.length <= 1) return "";
136+
const links = moduleFiles.map(({ mod, fileName }) =>
137+
mod === currentMod
138+
? `<strong>${escapeHtml(mod)}</strong>`
139+
: `<a href="./${fileName}">${escapeHtml(mod)}</a>`,
140+
);
141+
return `<nav aria-label="Modules"><p><small>${links.join(" &middot; ")}</small></p></nav>\n`;
83142
}
84143

85144
// Collect index entries as data, render at the end
@@ -96,6 +155,7 @@ interface IndexGameEntry {
96155
moduleCount: number;
97156
revision: number;
98157
versionDate: string;
158+
isoDate: string;
99159
modules: IndexModuleEntry[];
100160
}
101161
const indexEntries: IndexGameEntry[] = [];
@@ -124,12 +184,21 @@ for (const game of GAME_LIST) {
124184
}
125185
}
126186

187+
const isoDate = metadata.versionDate ? toIsoDate(metadata.versionDate, metadata.versionTime) : "";
127188
const indexModules: IndexModuleEntry[] = [];
128189

129190
const modAppRoot = "../..";
130191
const gameDir = resolve(sitemapDir, game.id);
131192
mkdirSync(gameDir, { recursive: true });
132193

194+
// First pass: compute fragments and chunks for all modules
195+
interface ModuleChunkData {
196+
mod: string;
197+
itemCount: number;
198+
chunks: string[][];
199+
}
200+
const moduleChunks: ModuleChunkData[] = [];
201+
133202
for (const mod of modules) {
134203
const items = byModule.get(mod)!;
135204

@@ -181,8 +250,19 @@ for (const game of GAME_LIST) {
181250
chunkSize += fragSize;
182251
}
183252

184-
const needsChunking = chunks.length > 1;
185253
indexModules.push({ mod, count: items.length, chunkCount: chunks.length });
254+
moduleChunks.push({ mod, itemCount: items.length, chunks });
255+
}
256+
257+
// Build sibling module nav links
258+
const moduleFiles = moduleChunks.map(({ mod, chunks }) => ({
259+
mod,
260+
fileName: chunks.length > 1 ? `${mod}_1.html` : `${mod}.html`,
261+
}));
262+
263+
// Second pass: write HTML files with sibling nav
264+
for (const { mod, itemCount, chunks } of moduleChunks) {
265+
const needsChunking = chunks.length > 1;
186266

187267
for (let i = 0; i < chunks.length; i++) {
188268
const suffix = needsChunking ? `_${i + 1}` : "";
@@ -203,26 +283,34 @@ for (const game of GAME_LIST) {
203283
pagination = `<nav aria-label="Pagination">Pages: ${links.join(" ")}</nav>\n`;
204284
}
205285

206-
let body = `<nav aria-label="Breadcrumb"><a href="../">Sitemap</a> &rsaquo; <a href="${modAppRoot}/#/${game.id}">${escapeHtml(game.name)}</a> &rsaquo; ${escapeHtml(mod)}</nav>
286+
const bc = breadcrumb([
287+
{ name: "Sitemap", href: "../" },
288+
{ name: game.name, href: `${modAppRoot}/#/${game.id}` },
289+
{ name: mod },
290+
]);
291+
292+
let body = `${bc}
293+
${banner(modAppRoot, metadata.revision, metadata.versionDate, isoDate)}
207294
<header>
208-
<h1><a href="${modAppRoot}/#/${game.id}/${mod}">${escapeHtml(mod)}</a></h1>
209-
<p><small>${items.length} declarations in ${escapeHtml(game.name)}${pageNum}</small></p>
210-
${banner(modAppRoot)}
295+
<h1 itemprop="name"><a href="${modAppRoot}/#/${game.id}/${mod}">${escapeHtml(mod)}</a></h1>
296+
<p><small>${itemCount} declarations in ${escapeHtml(game.name)}${pageNum}</small></p>
211297
</header>
212298
${pagination}<main>
213299
<dl>
214300
${chunks[i].join("")}</dl>
215301
</main>
216-
${pagination}`;
302+
${pagination}${moduleNav(moduleFiles, mod)}`;
217303

304+
const canonicalUrl = `${siteOrigin}${basePath}/sitemap/${game.id}/${fileName}`;
218305
const moduleHtml = htmlPage(
219306
`${mod}${pageNum} - ${game.name}`,
220307
`All classes and enums in the ${mod} module for ${game.name} Source 2 schemas.`,
221308
body,
309+
{ isoDate: isoDate || undefined, canonicalUrl, schemaType: "TechArticle" },
222310
);
223311
writeFileSync(resolve(gameDir, fileName), moduleHtml);
224312
totalFiles++;
225-
sitemapXmlUrls.push(`${siteOrigin}${basePath}/sitemap/${game.id}/${fileName}`);
313+
sitemapXmlUrls.push({ url: canonicalUrl, lastmod: isoDate || undefined });
226314
}
227315
}
228316

@@ -234,14 +322,17 @@ ${pagination}`;
234322
moduleCount: modules.length,
235323
revision: metadata.revision,
236324
versionDate: metadata.versionDate,
325+
isoDate,
237326
modules: indexModules,
238327
});
239328
}
240329

241330
// --- Render index page from collected data ---
242-
let indexBody = `<header>
243-
<h1><a href="${appRoot}/">Source 2 Schema Explorer</a></h1>
331+
const indexBc = breadcrumb([{ name: "Schema Explorer", href: `${appRoot}/` }, { name: "Sitemap" }]);
332+
let indexBody = `${indexBc}
244333
${banner(appRoot)}
334+
<header>
335+
<h1 itemprop="name"><a href="${appRoot}/">Source 2 Schema Explorer</a></h1>
245336
</header>
246337
<main>
247338
`;
@@ -251,7 +342,8 @@ for (const entry of indexEntries) {
251342
<h2><a href="${appRoot}/#/${entry.game.id}">${escapeHtml(entry.game.name)}</a></h2>
252343
<p><small>${entry.classCount.toLocaleString()} classes, ${entry.enumCount.toLocaleString()} enums, ${entry.fieldCount.toLocaleString()} fields/members across ${entry.moduleCount} modules`;
253344
if (entry.revision) indexBody += ` &middot; Revision ${entry.revision}`;
254-
if (entry.versionDate) indexBody += ` &middot; ${escapeHtml(entry.versionDate)}`;
345+
if (entry.versionDate)
346+
indexBody += ` &middot; <time datetime="${entry.isoDate}">${escapeHtml(entry.versionDate)}</time>`;
255347
indexBody += `</small></p>
256348
<ul>
257349
`;
@@ -275,21 +367,29 @@ for (const entry of indexEntries) {
275367
indexBody += `</main>`;
276368

277369
mkdirSync(sitemapDir, { recursive: true });
370+
const latestIsoDate = indexEntries.reduce(
371+
(latest, e) => (e.isoDate > latest ? e.isoDate : latest),
372+
"",
373+
);
374+
const indexCanonical = `${siteOrigin}${basePath}/sitemap/`;
278375
const indexHtml = htmlPage(
279376
"Sitemap",
280377
"Complete index of all Source 2 engine schema classes and enums for Counter-Strike 2, Dota 2, and Deadlock.",
281378
indexBody,
379+
{ isoDate: latestIsoDate || undefined, canonicalUrl: indexCanonical },
282380
);
283381
writeFileSync(resolve(sitemapDir, "index.html"), indexHtml);
284382
totalFiles++;
285-
sitemapXmlUrls.unshift(`${siteOrigin}${basePath}/sitemap/`);
383+
sitemapXmlUrls.unshift({ url: `${siteOrigin}${basePath}/sitemap/` });
286384

287385
// --- sitemap.xml ---
288386
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
289387
xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
290388
xml += `<url><loc>${siteOrigin}${basePath}/</loc></url>\n`;
291-
for (const url of sitemapXmlUrls) {
292-
xml += `<url><loc>${escapeHtml(url)}</loc></url>\n`;
389+
for (const { url, lastmod } of sitemapXmlUrls) {
390+
xml += `<url><loc>${escapeHtml(url)}</loc>`;
391+
if (lastmod) xml += `<lastmod>${lastmod}</lastmod>`;
392+
xml += `</url>\n`;
293393
}
294394
xml += `</urlset>\n`;
295395
writeFileSync(resolve(distDir, "sitemap.xml"), xml);

src/components/Docs/SchemaClass.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,19 @@ export const SchemaClassView: React.FC<{
137137
const { root, classesByKey } = useContext(DeclarationsContext);
138138
const navigate = useNavigate();
139139
const parsed = useParsedSearch();
140-
const { nameWords: searchWords, offsets: searchOffsets, metadataKeys: searchMetadata, metadataValues: searchMetadataValues } = parsed;
140+
const {
141+
nameWords: searchWords,
142+
offsets: searchOffsets,
143+
metadataKeys: searchMetadata,
144+
metadataValues: searchMetadataValues,
145+
} = parsed;
141146
const fieldParam = useFieldParam();
142147

143-
const isSearching = searchWords.length > 0 || searchOffsets.size > 0 || searchMetadata.length > 0 || searchMetadataValues.length > 0;
148+
const isSearching =
149+
searchWords.length > 0 ||
150+
searchOffsets.size > 0 ||
151+
searchMetadata.length > 0 ||
152+
searchMetadataValues.length > 0;
144153

145154
const inheritedGroups = useMemo(() => {
146155
if (isSearching) return [];
@@ -197,7 +206,8 @@ export const SchemaClassView: React.FC<{
197206
</DeclarationHeader>
198207
{(!collapseNonMatching ||
199208
(searchMetadata.length > 0 && matchesMetadataKeys(declaration.metadata, searchMetadata)) ||
200-
(searchMetadataValues.length > 0 && matchesMetadataValues(declaration.metadata, searchMetadataValues)) ||
209+
(searchMetadataValues.length > 0 &&
210+
matchesMetadataValues(declaration.metadata, searchMetadataValues)) ||
201211
(searchWords.length > 0 && matchesMetadataKeys(declaration.metadata, searchWords))) && (
202212
<MetadataTags metadata={declaration.metadata} root={root} navigate={navigate} />
203213
)}

src/components/Docs/SchemaEnum.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,15 @@ export const SchemaEnumView: React.FC<{
7777
const { root } = useContext(DeclarationsContext);
7878
const navigate = useNavigate();
7979
const parsed = useParsedSearch();
80-
const { nameWords: searchWords, metadataKeys: searchMetadata, metadataValues: searchMetadataValues } = parsed;
80+
const {
81+
nameWords: searchWords,
82+
metadataKeys: searchMetadata,
83+
metadataValues: searchMetadataValues,
84+
} = parsed;
8185
const fieldParam = useFieldParam();
8286

83-
const isSearching = searchWords.length > 0 || searchMetadata.length > 0 || searchMetadataValues.length > 0;
87+
const isSearching =
88+
searchWords.length > 0 || searchMetadata.length > 0 || searchMetadataValues.length > 0;
8489
const nameMatches = searchWords.length > 0 && matchesWords(declaration.name, searchWords);
8590
const collapseNonMatching = isSearching && !nameMatches;
8691

@@ -108,7 +113,8 @@ export const SchemaEnumView: React.FC<{
108113
</DeclarationHeader>
109114
{(!collapseNonMatching ||
110115
(searchMetadata.length > 0 && matchesMetadataKeys(declaration.metadata, searchMetadata)) ||
111-
(searchMetadataValues.length > 0 && matchesMetadataValues(declaration.metadata, searchMetadataValues)) ||
116+
(searchMetadataValues.length > 0 &&
117+
matchesMetadataValues(declaration.metadata, searchMetadataValues)) ||
112118
(searchWords.length > 0 && matchesMetadataKeys(declaration.metadata, searchWords))) && (
113119
<MetadataTags metadata={declaration.metadata} root={root} navigate={navigate} />
114120
)}

src/components/Docs/utils/filtering.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ function doSearch(declarations: api.Declaration[], parsed: ParsedSearch): api.De
176176
metadata?: api.SchemaMetadataEntry[],
177177
): boolean {
178178
return (
179-
name.includes(word) ||
180-
(metadata?.some((m) => m.name.toLowerCase().includes(word)) ?? false)
179+
name.includes(word) || (metadata?.some((m) => m.name.toLowerCase().includes(word)) ?? false)
181180
);
182181
}
183182

@@ -195,8 +194,10 @@ function doSearch(declarations: api.Declaration[], parsed: ParsedSearch): api.De
195194

196195
function matchesMetadata(declaration: api.Declaration): boolean {
197196
const allMetadata: (api.SchemaMetadataEntry[] | undefined)[] = [declaration.metadata];
198-
if (declaration.kind === "class") declaration.fields.forEach((f) => allMetadata.push(f.metadata));
199-
else if (declaration.kind === "enum") declaration.members.forEach((m) => allMetadata.push(m.metadata));
197+
if (declaration.kind === "class")
198+
declaration.fields.forEach((f) => allMetadata.push(f.metadata));
199+
else if (declaration.kind === "enum")
200+
declaration.members.forEach((m) => allMetadata.push(m.metadata));
200201

201202
const keysMatch =
202203
metadataKeys.length === 0 || allMetadata.some((md) => matchesMetadataKeys(md, metadataKeys));

0 commit comments

Comments
 (0)