Skip to content

Commit cc3f8c4

Browse files
louis-preclaude
andcommitted
Add cross-site link validator
Scans all markdown files for absolute docs.seam.co URLs and verifies the target page exists in the correct site section directory. Uses the centralized config for URL prefix → directory mapping. Run with: npm run validate-links Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 566104f commit cc3f8c4

4 files changed

Lines changed: 122 additions & 1 deletion

File tree

.github/workflows/check.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ jobs:
2121
uses: ./.github/actions/setup
2222
- name: Lint
2323
run: npm run lint
24+
validate-links:
25+
name: Validate links
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 30
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
- name: Setup
32+
uses: ./.github/actions/setup
33+
- name: Validate cross-site links
34+
run: npm run validate-links
2435
typecheck:
2536
name: Typecheck
2637
runs-on: ubuntu-latest

codegen/validate-links.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
4+
import {
5+
apiReferenceRoot,
6+
apiReferenceUrlPrefix,
7+
baseUrl,
8+
guidesRoot,
9+
siteUrlPrefix,
10+
} from './lib/config.js'
11+
12+
// Maps published URL prefixes to their site section root on disk.
13+
const siteSections: Array<{ urlPrefix: string; root: string }> = [
14+
{ urlPrefix: apiReferenceUrlPrefix, root: apiReferenceRoot },
15+
{ urlPrefix: '', root: guidesRoot },
16+
]
17+
18+
const urlPattern = new RegExp(
19+
`${baseUrl.replaceAll('.', '\\.')}[^)\\s]+`,
20+
'g',
21+
)
22+
23+
interface BrokenLink {
24+
file: string
25+
line: number
26+
url: string
27+
reason: string
28+
}
29+
30+
const brokenLinks: BrokenLink[] = []
31+
32+
function walkDir(dir: string): string[] {
33+
const files: string[] = []
34+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
35+
const fullPath = join(dir, entry.name)
36+
if (entry.isDirectory()) {
37+
files.push(...walkDir(fullPath))
38+
} else if (entry.name.endsWith('.md')) {
39+
files.push(fullPath)
40+
}
41+
}
42+
return files
43+
}
44+
45+
const files = walkDir('docs')
46+
47+
for (const file of files) {
48+
const contents = readFileSync(file, 'utf-8')
49+
const lines = contents.split('\n')
50+
51+
for (let i = 0; i < lines.length; i++) {
52+
const matches = lines[i]?.matchAll(urlPattern)
53+
if (matches == null) continue
54+
55+
for (const match of matches) {
56+
const rawUrl = match[0].replace(/[).,]*$/, '')
57+
const url = new URL(rawUrl)
58+
url.pathname = url.pathname.replace(siteUrlPrefix, '')
59+
60+
// Strip hash anchors for file resolution
61+
const pathname = url.pathname
62+
63+
const section = siteSections.find(({ urlPrefix }) =>
64+
urlPrefix === ''
65+
? true
66+
: pathname.startsWith(urlPrefix + '/') || pathname === urlPrefix,
67+
)
68+
69+
if (section == null) {
70+
brokenLinks.push({
71+
file,
72+
line: i + 1,
73+
url: rawUrl,
74+
reason: 'No matching site section',
75+
})
76+
continue
77+
}
78+
79+
const pagePath = pathname.replace(section.urlPrefix, '')
80+
const targetRoot = join(section.root, pagePath)
81+
82+
const targetMd = `${targetRoot}.md`
83+
const targetReadme = join(targetRoot, 'README.md')
84+
85+
if (!existsSync(targetMd) && !existsSync(targetReadme)) {
86+
brokenLinks.push({
87+
file,
88+
line: i + 1,
89+
url: rawUrl,
90+
reason: `File not found: ${targetMd} or ${targetReadme}`,
91+
})
92+
}
93+
}
94+
}
95+
}
96+
97+
if (brokenLinks.length > 0) {
98+
console.error(`Found ${brokenLinks.length} broken cross-site link(s):\n`)
99+
for (const { file, line, url, reason } of brokenLinks) {
100+
console.error(` ${file}:${line}`)
101+
console.error(` ${url}`)
102+
console.error(` ${reason}\n`)
103+
}
104+
process.exit(1)
105+
} else {
106+
console.log('All cross-site links are valid.')
107+
}

docs/api-reference/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
---
2+
description: Comprehensive reference for integrating with Seam API endpoints
3+
---
14

25
# Overview
36

@@ -29,4 +32,3 @@ See the following reference topics:
2932
* [Events](events/)
3033
* [Action Attempts](action_attempts/)
3134
* [Connected Accounts](connected_accounts/)
32-

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scripts": {
1717
"generate": "tsx codegen/smith.ts",
1818
"postgenerate": "npm run format",
19+
"validate-links": "tsx codegen/validate-links.ts",
1920
"typecheck": "tsc",
2021
"lint": "eslint .",
2122
"postlint": "prettier --check --ignore-path .prettierignore .",

0 commit comments

Comments
 (0)