ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
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.

public/sitemap.xmlmodified+3-0
Changes to public/sitemap.xml
@@ -45,4 +45,7 @@
4545 <url>
4646 <loc>https://www.lastsoulultra.com/faq</loc>
4747 </url>
48 <url>
49 <loc>https://www.lastsoulultra.com/coaching/plans</loc>
50 </url>
4851</urlset>
resources/data/site.tsmodified+4-0
Changes to resources/data/site.ts
@@ -1,4 +1,5 @@
11import { env } from '@stacksjs/env'
2import { indexableModulePaths } from './coaching'
23import { race } from './race'
34
45/**
@@ -45,6 +46,9 @@ export const indexablePaths: string[] = [
4546 '/', '/format', '/world-series', '/soul-races', '/sponsorship', '/contact',
4647 '/lottery', '/results', '/results/last-soul-ultra-2026', '/results/last-soul-ultra-2025',
4748 '/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(),
4852]
4953
5054/** "14 august 2026", lowercase like the rest of the site's voice. */
tests/seo.test.tsmodified+38-8
Changes to tests/seo.test.ts
@@ -1,6 +1,9 @@
11import { describe, expect, it } from 'bun:test'
22import { readdirSync } from 'node:fs'
3import { join } from 'node:path'
4import { coachingPage } from '../resources/data/coaching'
35import { legalPages } from '../resources/data/legal'
6import { productionPages } from '../resources/data/production-pages'
47import { indexablePaths } from '../resources/data/site'
58import { publishedOrigin, robotsTxt, sitemapXml } from '../scripts/seo'
69
@@ -22,21 +25,48 @@ describe('seo artefacts', () => {
2225 expect(committed).toBe(robotsTxt(publishedOrigin))
2326 })
2427
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
2549 it('lists every view that is not dynamic, private or noindex', () => {
2650 // 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))
3553
54 expect(expected.length).toBeGreaterThan(14)
3655 for (const path of expected)
3756 expect(indexablePaths).toContain(path)
3857 })
3958
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
4070 it('lists the canonical legal pages rather than the legacy redirects', () => {
4171 for (const page of legalPages)
4272 expect(indexablePaths).toContain(`/${page.slug}`)