also looking at this
LSU's own coaching surface, and what auditing the site turned up
#12
86 files
+3943
-1190
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { env } from '@stacksjs/env' |
| 2 | import { indexableModulePaths } from './coaching' | |
| 2 | 3 | import { race } from './race' |
| 3 | 4 | |
| 4 | 5 | /** |
| @@ -45,6 +46,9 @@ export const indexablePaths: string[] = [ | ||
| 45 | 46 | '/', '/format', '/world-series', '/soul-races', '/sponsorship', '/contact', |
| 46 | 47 | '/lottery', '/results', '/results/last-soul-ultra-2026', '/results/last-soul-ultra-2025', |
| 47 | 48 | '/imprint', '/privacy', '/terms', '/coaching', '/faq', |
| 49 | // The storefront only. The other seven training modules show a sign-in | |
| 50 | // prompt to anyone without an account, which is not a search result. | |
| 51 | ...indexableModulePaths(), | |
| 48 | 52 | ] |
| 49 | 53 | |
| 50 | 54 | /** "14 august 2026", lowercase like the rest of the site's voice. */ |
| @@ -1,6 +1,9 @@ | ||
| 1 | 1 | import { describe, expect, it } from 'bun:test' |
| 2 | 2 | import { readdirSync } from 'node:fs' |
| 3 | import { join } from 'node:path' | |
| 4 | import { coachingPage } from '../resources/data/coaching' | |
| 3 | 5 | import { legalPages } from '../resources/data/legal' |
| 6 | import { productionPages } from '../resources/data/production-pages' | |
| 4 | 7 | import { indexablePaths } from '../resources/data/site' |
| 5 | 8 | import { publishedOrigin, robotsTxt, sitemapXml } from '../scripts/seo' |
| 6 | 9 | |
| @@ -22,21 +25,48 @@ describe('seo artefacts', () => { | ||
| 22 | 25 | expect(committed).toBe(robotsTxt(publishedOrigin)) |
| 23 | 26 | }) |
| 24 | 27 | |
| 28 | /** Every view on disk, as the path the site serves it at. */ | |
| 29 | function viewPaths(dir = 'resources/views', prefix = ''): string[] { | |
| 30 | return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { | |
| 31 | if (entry.isDirectory()) | |
| 32 | return viewPaths(join(dir, entry.name), `${prefix}/${entry.name}`) | |
| 33 | if (!entry.name.endsWith('.stx')) | |
| 34 | return [] | |
| 35 | const name = entry.name.slice(0, -'.stx'.length) | |
| 36 | return [name === 'index' && prefix === '' ? '/' : `${prefix}/${name}`] | |
| 37 | }) | |
| 38 | } | |
| 39 | ||
| 40 | /** A page that declares itself noindex is not expected in the sitemap. */ | |
| 41 | function noindex(path: string): boolean { | |
| 42 | if (path === '/404') | |
| 43 | return true | |
| 44 | if (path.startsWith('/coaching/')) | |
| 45 | return coachingPage(path.slice('/coaching/'.length)).noindex === true | |
| 46 | return productionPages[path]?.noindex === true | |
| 47 | } | |
| 48 | ||
| 25 | 49 | it('lists every view that is not dynamic, private or noindex', () => { |
| 26 | 50 | // Views on disk are the ground truth for what the site actually serves. |
| 27 | const views = readdirSync('resources/views') | |
| 28 | .filter(name => name.endsWith('.stx')) | |
| 29 | .map(name => name.slice(0, -'.stx'.length)) | |
| 30 | ||
| 31 | const expected = views | |
| 32 | // 404 is noindex on purpose, and index is the root path. | |
| 33 | .filter(name => !['404', 'profile'].includes(name)) | |
| 34 | .map(name => (name === 'index' ? '/' : `/${name}`)) | |
| 51 | // Recursive: /results and /coaching both hold pages a flat read missed. | |
| 52 | const expected = viewPaths().filter(path => !noindex(path)) | |
| 35 | 53 | |
| 54 | expect(expected.length).toBeGreaterThan(14) | |
| 36 | 55 | for (const path of expected) |
| 37 | 56 | expect(indexablePaths).toContain(path) |
| 38 | 57 | }) |
| 39 | 58 | |
| 59 | it('keeps the modules that need an account out of the sitemap', () => { | |
| 60 | const modules = viewPaths().filter(path => path.startsWith('/coaching/')) | |
| 61 | ||
| 62 | expect(modules).toHaveLength(8) | |
| 63 | for (const path of modules) { | |
| 64 | if (noindex(path)) | |
| 65 | expect(indexablePaths).not.toContain(path) | |
| 66 | } | |
| 67 | expect(indexablePaths).toContain('/coaching/plans') | |
| 68 | }) | |
| 69 | ||
| 40 | 70 | it('lists the canonical legal pages rather than the legacy redirects', () => { |
| 41 | 71 | for (const page of legalPages) |
| 42 | 72 | expect(indexablePaths).toContain(`/${page.slug}`) |