@@ -27,6 +27,11 @@ function escapeHtml(s: string): string {
2727 . replace ( / " / g, """ ) ;
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+
3035function 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; }
5686dt { margin-top: 0.5em; }
5787dd { margin-left: 1.5em; }
5888table { border-collapse: collapse; width: 100%; margin: 4px 0 0 1.5em; }
5989th, td { text-align: left; padding: 2px 8px; border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent); font-family: monospace; }
6090th { 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
76106let totalFiles = 0 ;
77- const sitemapXmlUrls : string [ ] = [ ] ;
107+ const sitemapXmlUrls : { url : string ; lastmod ?: string } [ ] = [ ] ;
78108
79109const 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 } · <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 ( " › " ) ;
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 ( " · " ) } </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}
101161const 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> › <a href="${ modAppRoot } /#/${ game . id } ">${ escapeHtml ( game . name ) } </a> › ${ 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 += ` · Revision ${ entry . revision } ` ;
254- if ( entry . versionDate ) indexBody += ` · ${ escapeHtml ( entry . versionDate ) } ` ;
345+ if ( entry . versionDate )
346+ indexBody += ` · <time datetime="${ entry . isoDate } ">${ escapeHtml ( entry . versionDate ) } </time>` ;
255347 indexBody += `</small></p>
256348<ul>
257349` ;
@@ -275,21 +367,29 @@ for (const entry of indexEntries) {
275367indexBody += `</main>` ;
276368
277369mkdirSync ( 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/` ;
278375const 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) ;
283381writeFileSync ( resolve ( sitemapDir , "index.html" ) , indexHtml ) ;
284382totalFiles ++ ;
285- sitemapXmlUrls . unshift ( `${ siteOrigin } ${ basePath } /sitemap/` ) ;
383+ sitemapXmlUrls . unshift ( { url : `${ siteOrigin } ${ basePath } /sitemap/` } ) ;
286384
287385// --- sitemap.xml ---
288386let xml = `<?xml version="1.0" encoding="UTF-8"?>\n` ;
289387xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` ;
290388xml += `<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}
294394xml += `</urlset>\n` ;
295395writeFileSync ( resolve ( distDir , "sitemap.xml" ) , xml ) ;
0 commit comments