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+0-3
Changes to public/sitemap.xml
@@ -42,9 +42,6 @@
4242 <url>
4343 <loc>https://www.lastsoulultra.com/coaching</loc>
4444 </url>
45 <url>
46 <loc>https://www.lastsoulultra.com/faq</loc>
47 </url>
4845 <url>
4946 <loc>https://www.lastsoulultra.com/coaching/plans</loc>
5047 </url>
resources/data/site.tsmodified+6-1
Changes to resources/data/site.ts
@@ -45,7 +45,12 @@ export function absolute(path: string): string {
4545export const indexablePaths: string[] = [
4646 '/', '/format', '/world-series', '/soul-races', '/sponsorship', '/contact',
4747 '/lottery', '/results', '/results/last-soul-ultra-2026', '/results/last-soul-ultra-2025',
48 '/imprint', '/privacy', '/terms', '/coaching', '/faq',
48 '/imprint', '/privacy', '/terms', '/coaching',
49 // /faq is served, and stays served: it is a URL the previous site published
50 // and other people's links still point at. It is not listed, because it
51 // renders the contact page and canonicalises to /contact - advertising a
52 // URL that tells a crawler it is not the canonical one asks for the page to
53 // be ignored and spends the site's crawl budget doing it.
4954 // The storefront only. The other seven training modules show a sign-in
5055 // prompt to anyone without an account, which is not a search result.
5156 ...indexableModulePaths(),
tests/seo.test.tsmodified+33-3
Changes to tests/seo.test.ts
@@ -1,5 +1,5 @@
11import { describe, expect, it } from 'bun:test'
2import { readdirSync } from 'node:fs'
2import { readFileSync, readdirSync } from 'node:fs'
33import { join } from 'node:path'
44import { coachingPage } from '../resources/data/coaching'
55import { legalPages } from '../resources/data/legal'
@@ -37,6 +37,22 @@ describe('seo artefacts', () => {
3737 })
3838 }
3939
40 /**
41 * The canonical a view declares, read from its source. Views write it as a
42 * literal, and the coaching modules build it from their slug.
43 */
44 function canonicalOf(path: string): string {
45 const file = `resources/views${path === '/' ? '/index' : path}.stx`
46 const source = readFileSync(file, 'utf8')
47 const literal = /const canonicalPath = ['"]([^'"]+)['"]/.exec(source)
48 if (literal)
49 return literal[1]!
50 const built = /const canonicalPath = modulePath\(['"]([^'"]+)['"]\)/.exec(source)
51 if (built)
52 return `/coaching/${built[1]}`
53 throw new Error(`${file} declares no canonicalPath`)
54 }
55
4056 /** A page that declares itself noindex is not expected in the sitemap. */
4157 function noindex(path: string): boolean {
4258 if (path === '/404')
@@ -49,13 +65,27 @@ describe('seo artefacts', () => {
4965 it('lists every view that is not dynamic, private or noindex', () => {
5066 // Views on disk are the ground truth for what the site actually serves.
5167 // Recursive: /results and /coaching both hold pages a flat read missed.
52 const expected = viewPaths().filter(path => !noindex(path))
68 const expected = viewPaths().filter(path => !noindex(path) && canonicalOf(path) === path)
5369
54 expect(expected.length).toBeGreaterThan(14)
70 expect(expected.length).toBeGreaterThan(13)
5571 for (const path of expected)
5672 expect(indexablePaths).toContain(path)
5773 })
5874
75 it('does not list a page that points its canonical somewhere else', () => {
76 // /faq renders the contact page and canonicalises to /contact. Listing a
77 // URL that tells a crawler it is not the canonical one asks for the page
78 // to be ignored, and spends crawl budget doing it.
79 const aliases = viewPaths().filter(path => !noindex(path) && canonicalOf(path) !== path)
80
81 expect(aliases).toContain('/faq')
82 for (const path of aliases) {
83 expect(indexablePaths).not.toContain(path)
84 // An alias must still point at a page the sitemap does list.
85 expect(indexablePaths).toContain(canonicalOf(path))
86 }
87 })
88
5989 it('keeps the modules that need an account out of the sitemap', () => {
6090 const modules = viewPaths().filter(path => path.startsWith('/coaching/'))
6191