Skip to content

Commit cfd57eb

Browse files
committed
fix dead links
1 parent 890dc4b commit cfd57eb

5 files changed

Lines changed: 110 additions & 5 deletions

File tree

docs/sdk/command-line-debugging.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ curl -p -x http://127.0.0.1:8080 https://ipinfo.io
6464

6565
The Outline SDK allows for the specification of various circumvention strategies
6666
that can be combined to bypass different forms of network interference. The
67-
specification for these strategies is in the [go documentation](https://pkg.go.dev/github.com/OutlineFoundation/outline-sdk/x@v0.0.3/configurl).
67+
specification for these strategies is in the [go documentation](https://pkg.go.dev/golang.getoutline.org/sdk/x/configurl).
6868

6969
### Composable Strategies
7070

docs/sdk/mobile-app-integration.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ client.findProxy = (Uri uri) {
279279
<TabItem value="okhttp" label="OkHttp (Android)">
280280

281281
Set the proxy with
282-
[`OkHttpClient.Builder.proxy`](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/proxy/).
282+
[`OkHttpClient.Builder.proxy`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/proxy.html).
283283

284284
```kotlin
285285
val proxyConfig = Proxy(Proxy.Type.HTTP, InetSocketAddress(proxy.host(), proxy.port()))

docs/sdk/reference/smart-dialer-config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ tcp:
8585
* Each TLS transport is a string that specifies the transport to use.
8686
* For example, `override:host=cloudflare.net|tlsfrag:1` specifies a transport
8787
that uses domain fronting with Cloudflare and TLS fragmentation. See the
88-
[config documentation](https://pkg.go.dev/github.com/OutlineFoundation/outline-sdk/x/configurl#hdr-Config_Format)
88+
[config documentation](https://pkg.go.dev/golang.getoutline.org/sdk/x/configurl#hdr-Config_Format)
8989
for details.
9090

9191
### Fallback Configuration
@@ -97,7 +97,7 @@ DNS/TLS strategies must fail/timeout.
9797

9898
The fallback strings should be:
9999

100-
* A valid `StreamDialer` config string as defined in [`configurl`](https://pkg.go.dev/github.com/OutlineFoundation/outline-sdk/x/configurl#hdr-Proxy_Protocols).
100+
* A valid `StreamDialer` config string as defined in [`configurl`](https://pkg.go.dev/golang.getoutline.org/sdk/x/configurl#hdr-Proxy_Protocols).
101101
* A valid Psiphon configuration object as a child of a `psiphon` field.
102102

103103
#### Shadowsocks server example

docs/vpn/advanced/prefixing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ sidebar_label: "Connection Prefixes"
77

88
As of Outline Client version 1.9.0, access keys support the "prefix" option. The
99
"prefix" is a list of bytes used as the first bytes of the
10-
[salt](https://shadowsocks.org/guide/aead.html) of a Shadowsocks TCP connection.
10+
[salt](https://shadowsocks.org/doc/aead.html) of a Shadowsocks TCP connection.
1111
This can make the connection look like a protocol that is allowed in the
1212
network, circumventing firewalls that reject protocols they don't recognize.
1313

scripts/check_links.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python3
2+
"""Check external URLs in English docs for dead links."""
3+
4+
import re
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
from concurrent.futures import ThreadPoolExecutor, as_completed
9+
10+
DOCS_DIR = Path(__file__).parent.parent / "docs"
11+
DOC_EXTENSIONS = (".md", ".mdx")
12+
13+
# URLs to skip (templates with placeholders, localhost, etc.)
14+
SKIP_PATTERNS = [
15+
"<", # URLs with placeholder variables like <DOMAIN_NAME>
16+
"localhost",
17+
"127.0.0.1",
18+
"1.1.1.1",
19+
"example.com",
20+
]
21+
22+
23+
def extract_urls(text: str) -> list[str]:
24+
"""Extract all http/https URLs from markdown text."""
25+
urls = set()
26+
# Markdown links [text](url)
27+
for m in re.finditer(r'\[(?:[^\]\\]|\\.)*\]\((https?://[^)]+)\)', text):
28+
urls.add(m.group(1))
29+
# Auto-links <url>
30+
for m in re.finditer(r'<(https?://[^>]+)>', text):
31+
urls.add(m.group(1))
32+
return sorted(urls)
33+
34+
35+
def should_skip(url: str) -> bool:
36+
for pattern in SKIP_PATTERNS:
37+
if pattern in url:
38+
return True
39+
return False
40+
41+
42+
def find_urls_in_docs() -> dict[str, list[str]]:
43+
"""Return {url: [files]} mapping for all external URLs in docs."""
44+
url_files: dict[str, list[str]] = {}
45+
for ext in DOC_EXTENSIONS:
46+
for f in DOCS_DIR.rglob(f"*{ext}"):
47+
text = f.read_text(encoding="utf-8")
48+
rel = str(f.relative_to(DOCS_DIR.parent))
49+
for url in extract_urls(text):
50+
if not should_skip(url):
51+
url_files.setdefault(url, []).append(rel)
52+
return url_files
53+
54+
55+
def check_url(url: str) -> tuple[str, int | str]:
56+
"""Check a URL with curl and return (url, status_code_or_error)."""
57+
try:
58+
result = subprocess.run(
59+
[
60+
"curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}",
61+
"-L", # follow redirects
62+
"--max-time", "15",
63+
"-A", "Mozilla/5.0 (link checker)",
64+
url,
65+
],
66+
capture_output=True,
67+
text=True,
68+
timeout=20,
69+
)
70+
code = int(result.stdout.strip())
71+
return url, code
72+
except Exception as e:
73+
return url, str(e)
74+
75+
76+
def main():
77+
url_files = find_urls_in_docs()
78+
urls = sorted(url_files.keys())
79+
print(f"Checking {len(urls)} unique external URLs...\n")
80+
81+
dead = []
82+
ok = 0
83+
with ThreadPoolExecutor(max_workers=8) as pool:
84+
futures = {pool.submit(check_url, url): url for url in urls}
85+
for future in as_completed(futures):
86+
url, result = future.result()
87+
if isinstance(result, int) and 200 <= result < 400:
88+
ok += 1
89+
else:
90+
files = url_files[url]
91+
dead.append((url, result, files))
92+
print(f" DEAD [{result}] {url}")
93+
for f in files:
94+
print(f" in {f}")
95+
96+
print(f"\n{ok} OK, {len(dead)} dead")
97+
if dead:
98+
sys.exit(1)
99+
else:
100+
print("PASSED: All external links are alive")
101+
sys.exit(0)
102+
103+
104+
if __name__ == "__main__":
105+
main()

0 commit comments

Comments
 (0)