-
-
Notifications
You must be signed in to change notification settings - Fork 0
Redirect the Oullin writing archive #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,3 +147,77 @@ func TestProdCaddyfileKeepsSignatureEndpointBehindMTLS(t *testing.T) { | |
| t.Fatal("expected /api/generate-signature* to be handled by api:8080 inside the :8443 mTLS listener") | ||
| } | ||
| } | ||
|
|
||
| func TestProdCaddyfileRedirectsWritingArchiveBeforeDefaultProxy(t *testing.T) { | ||
| caddyfile := stripCaddyComments(readProdCaddyfile(t)) | ||
| oullinBlock, ok := caddyBlock(caddyfile, "oullin.io") | ||
| if !ok { | ||
| t.Fatal("expected production Caddyfile to contain the oullin.io site") | ||
| } | ||
|
|
||
| redirects := []struct { | ||
| source string | ||
| target string | ||
| }{ | ||
| {"/post/2026-02-19-local-first-skills-platform-for-ai-agents", "https://writing.gocanto.sh/local-first-skills-platform-for-ai-agents"}, | ||
| {"/post/2023-06-21-bugs-are-inevitable-in-software-development", "https://writing.gocanto.sh/bugs-are-inevitable-in-software-development"}, | ||
| {"/post/2025-09-25-shipping-seo-for-a-single-page-app-the-pragmatic-way", "https://writing.gocanto.sh/shipping-seo-for-a-single-page-app-the-pragmatic-way"}, | ||
| {"/post/2025-10-09-when-a-real-time-feature-store-is-the-wrong-fix-for-fraud", "https://writing.gocanto.sh/when-a-real-time-feature-store-is-the-wrong-fix-for-fraud"}, | ||
| {"/post/2025-11-04-your-ai-model-is-not-your-product", "https://writing.gocanto.sh/your-ai-model-is-not-your-product"}, | ||
| {"/post/2026-04-05-go-meets-the-monolith-renaissance", "https://writing.gocanto.sh/go-meets-the-monolith-renaissance"}, | ||
| {"/post/2025-12-29-nostalgia-is-a-security-risk", "https://writing.gocanto.sh/nostalgia-is-a-security-risk"}, | ||
| {"/post/2025-11-21-demystifying-the-go-maps-engine", "https://writing.gocanto.sh/demystifying-the-go-maps-engine"}, | ||
| {"/post/2025-12-15-money-in-Go-done-properly", "https://writing.gocanto.sh/money-in-go-done-properly"}, | ||
| {"/post/2026-03-13-taming-complex-state-in-go", "https://writing.gocanto.sh/taming-complex-state-in-go"}, | ||
| {"/post/2026-03-31-laravel-collections-to-go", "https://writing.gocanto.sh/laravel-collections-to-go"}, | ||
| {"/post/2025-10-29-turbocharging-web-performance-automated-early-hints", "https://writing.gocanto.sh/turbocharging-web-performance-automated-early-hints"}, | ||
| {"/post/2025-11-13-shipping-observability-for-oullin-infrastructure", "https://writing.gocanto.sh/shipping-observability-for-oullin-infrastructure"}, | ||
| {"/post/2025-11-26-the-case-of-the-mismatched-clocks-lesson-secure-timing", "https://writing.gocanto.sh/the-case-of-the-mismatched-clocks-lesson-secure-timing"}, | ||
| {"/post/2026-02-26-stop-installing-python-just-to-convert-files-to-markdown", "https://writing.gocanto.sh/stop-installing-python-just-to-convert-files-to-markdown"}, | ||
| {"/post/2025-11-15-debugging-multi-layered-docker-deployment", "https://writing.gocanto.sh/debugging-multi-layered-docker-deployment"}, | ||
| {"/post/2023-06-22-positive-emotions-reduce-negative-emotions", "https://writing.gocanto.sh/positive-emotions-reduce-negative-emotions"}, | ||
| {"/post/2025-04-02-embrace-growth-through-movement", "https://writing.gocanto.sh/embrace-growth-through-movement"}, | ||
| {"/post/2025-10-17-the-operating-system-of-you-an-engineering-leader-reflection", "https://writing.gocanto.sh/the-operating-system-of-you-an-engineering-leader-reflection"}, | ||
| {"/post/2025-11-03-honesty-establishes-trust-enabling-process-formation-progression", "https://writing.gocanto.sh/honesty-establishes-trust-enabling-process-formation-progression"}, | ||
| {"/post/2025-11-30-work-is-not-family-and-that-is-a-good-thing", "https://writing.gocanto.sh/work-is-not-family-and-that-is-a-good-thing"}, | ||
| {"/post/2026-02-24-engineering-management-mistakes-we-need-to-stop-making.md", "https://writing.gocanto.sh/engineering-management-mistakes-we-need-to-stop-making"}, | ||
| {"/post/2025-12-05-handling-money-in-php", "https://writing.gocanto.sh/handling-money-in-php"}, | ||
| {"/post/2025-10-15-building-oullin", "https://writing.gocanto.sh/building-oullin"}, | ||
| } | ||
|
|
||
| defaultProxy := strings.Index(oullinBlock, "reverse_proxy web:80") | ||
| if defaultProxy == -1 { | ||
| t.Fatal("expected oullin.io site to contain the default web proxy") | ||
| } | ||
|
|
||
| for _, redirect := range redirects { | ||
| t.Run(redirect.source, func(t *testing.T) { | ||
| directive := "redir " + redirect.source + " " + redirect.target + " 301" | ||
| position := strings.Index(oullinBlock, directive) | ||
| if position == -1 { | ||
| t.Fatalf("expected exact writing redirect %q", directive) | ||
| } | ||
| if position > defaultProxy { | ||
| t.Fatalf("expected %s redirect before the default web proxy", redirect.source) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| for _, directive := range []string{ | ||
| "redir /writing https://writing.gocanto.sh/ 301", | ||
| "redir /writing/ https://writing.gocanto.sh/ 301", | ||
| "redir /tags/* https://writing.gocanto.sh/ 301", | ||
| } { | ||
| position := strings.Index(oullinBlock, directive) | ||
| if position == -1 { | ||
| t.Fatalf("expected archive redirect %q", directive) | ||
| } | ||
| if position > defaultProxy { | ||
| t.Fatalf("expected archive redirect %q before the default web proxy", directive) | ||
| } | ||
| } | ||
|
|
||
| if strings.Contains(oullinBlock, "redir /post/*") { | ||
| t.Fatal("must not use a broad /post/* redirect") | ||
| } | ||
|
Comment on lines
+220
to
+222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: set -euo pipefail
git ls-files infra/caddy/caddyfile_prod_test.go
echo '---'
ast-grep outline infra/caddy/caddyfile_prod_test.go --view expanded
echo '---'
sed -n '180,250p' infra/caddy/caddyfile_prod_test.goRepository: oullin/api Length of output: 2793 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('infra/caddy/caddyfile_prod_test.go')
text = p.read_text()
for needle in ['redir /post/*', 'strings.Contains(oullinBlock', 'named matcher', '/post*']:
print(f'### {needle}')
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
print(i, line)
PYRepository: oullin/api Length of output: 342 🏁 Script executed: set -euo pipefail
# Inspect the caddy matcher docs snippet locally in case the repo vendors a reference or comment.
rg -n "redir /post\\*|/post/\\*|matcher" infra/caddy -SRepository: oullin/api Length of output: 550 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('infra/caddy/caddyfile_prod_test.go')
for i, line in enumerate(p.read_text().splitlines(), 1):
if 205 <= i <= 235:
print(f"{i:4d}: {line}")
PYRepository: oullin/api Length of output: 826 🏁 Script executed: set -euo pipefail
# Inspect the production Caddyfile and any related redirect definitions.
git ls-files infra/caddy
echo '---'
rg -n "redir /post|`@post`|/post\\*" infra/caddy -S
echo '---'
# Show the relevant production block if present.
python3 - <<'PY'
from pathlib import Path
for path in [Path('infra/caddy/Caddyfile'), Path('infra/caddy/caddyfile_prod_test.go')]:
if path.exists():
print(f'## {path}')
for i, line in enumerate(path.read_text().splitlines(), 1):
if 1 <= i <= 260 and ('/post' in line or 'redir' in line or '`@post`' in line or 'reverse_proxy' in line):
print(f"{i:4d}: {line}")
PYRepository: oullin/api Length of output: 8925 🌐 Web query:
💡 Result: In Caddy, path matching is exact by default [1][2]. To match a path prefix, you must append an asterisk () [1][3][2]. Understanding the difference between /post and /post/* is critical [1][3]: - /post* matches /post, /post/, /postbar, and /post/bar [1][3]. - /post/* matches /post/ and /post/bar, but does NOT match /post or /postbar [1][3]. For most use cases where you want to match a path and all its sub-paths (e.g., to redirect a directory), /post/* is generally the preferred and more predictable choice [1][3][4]. The redir directive is used to issue HTTP redirects [5][6]. It accepts an optional matcher and the target location [5][7]. Common patterns: To redirect a specific path and all its sub-paths to a new location while preserving the remaining URI: redir /post/* https://example.com/new-post{uri} To redirect everything: redir https://example.com{uri} If you are using handle_path, note that it implicitly strips the matched path prefix, which can simplify your configuration if you need to perform actions on the remaining path segment after the prefix [8]. Caddyfile directives are evaluated based on a specific order, and redir is placed very early in the request pipeline [5][6][9], meaning it will trigger before most other directives (like reverse_proxy) if it matches the request [5]. Citations:
🏁 Script executed: set -euo pipefail
# Check whether the repository includes tests or docs that spell out the intended matcher shape.
rg -n "must not use a broad /post|broad /post|writing redirect|matcher" . -SRepository: oullin/api Length of output: 959 Reject broad 🤖 Prompt for AI AgentsSource: MCP tools |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.