From aec6a188e9c3cbb4ea0fdfaae6e6d0cb97440676 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 27 Jul 2026 16:45:33 +0800 Subject: [PATCH 1/2] Redirect the Oullin writing archive --- infra/caddy/Caddyfile.prod | 29 +++++++++++++ infra/caddy/caddyfile_prod_test.go | 70 ++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/infra/caddy/Caddyfile.prod b/infra/caddy/Caddyfile.prod index 5cda8df0..7b1c6c4c 100644 --- a/infra/caddy/Caddyfile.prod +++ b/infra/caddy/Caddyfile.prod @@ -42,6 +42,35 @@ oullin.io { format json } + # --- Writing archive: every legacy post has an explicit canonical target + redir /post/2026-02-19-local-first-skills-platform-for-ai-agents https://writing.gocanto.sh/local-first-skills-platform-for-ai-agents 301 + redir /post/2023-06-21-bugs-are-inevitable-in-software-development https://writing.gocanto.sh/bugs-are-inevitable-in-software-development 301 + redir /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 301 + redir /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 301 + redir /post/2025-11-04-your-ai-model-is-not-your-product https://writing.gocanto.sh/your-ai-model-is-not-your-product 301 + redir /post/2026-04-05-go-meets-the-monolith-renaissance https://writing.gocanto.sh/go-meets-the-monolith-renaissance 301 + redir /post/2025-12-29-nostalgia-is-a-security-risk https://writing.gocanto.sh/nostalgia-is-a-security-risk 301 + redir /post/2025-11-21-demystifying-the-go-maps-engine https://writing.gocanto.sh/demystifying-the-go-maps-engine 301 + redir /post/2025-12-15-money-in-Go-done-properly https://writing.gocanto.sh/money-in-go-done-properly 301 + redir /post/2026-03-13-taming-complex-state-in-go https://writing.gocanto.sh/taming-complex-state-in-go 301 + redir /post/2026-03-31-laravel-collections-to-go https://writing.gocanto.sh/laravel-collections-to-go 301 + redir /post/2025-10-29-turbocharging-web-performance-automated-early-hints https://writing.gocanto.sh/turbocharging-web-performance-automated-early-hints 301 + redir /post/2025-11-13-shipping-observability-for-oullin-infrastructure https://writing.gocanto.sh/shipping-observability-for-oullin-infrastructure 301 + redir /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 301 + redir /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 301 + redir /post/2025-11-15-debugging-multi-layered-docker-deployment https://writing.gocanto.sh/debugging-multi-layered-docker-deployment 301 + redir /post/2023-06-22-positive-emotions-reduce-negative-emotions https://writing.gocanto.sh/positive-emotions-reduce-negative-emotions 301 + redir /post/2025-04-02-embrace-growth-through-movement https://writing.gocanto.sh/embrace-growth-through-movement 301 + redir /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 301 + redir /post/2025-11-03-honesty-establishes-trust-enabling-process-formation-progression https://writing.gocanto.sh/honesty-establishes-trust-enabling-process-formation-progression 301 + redir /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 301 + redir /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 301 + redir /post/2025-12-05-handling-money-in-php https://writing.gocanto.sh/handling-money-in-php 301 + redir /post/2025-10-15-building-oullin https://writing.gocanto.sh/building-oullin 301 + redir /writing https://writing.gocanto.sh/ 301 + redir /writing/ https://writing.gocanto.sh/ 301 + redir /tags/* https://writing.gocanto.sh/ 301 + # --- Public listener: block protected paths @protected_public path /api/generate-signature* /api/metrics handle @protected_public { diff --git a/infra/caddy/caddyfile_prod_test.go b/infra/caddy/caddyfile_prod_test.go index 1b1a8afa..ca7e0962 100644 --- a/infra/caddy/caddyfile_prod_test.go +++ b/infra/caddy/caddyfile_prod_test.go @@ -147,3 +147,73 @@ 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", + } { + if !strings.Contains(oullinBlock, directive) { + t.Fatalf("expected archive redirect %q", directive) + } + } + + if strings.Contains(oullinBlock, "redir /post/*") { + t.Fatal("must not use a broad /post/* redirect") + } +} From d553658ae55529cd8dcd6bca9a426b2189b59ace Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 27 Jul 2026 16:49:57 +0800 Subject: [PATCH 2/2] Assert archive redirects precede the proxy --- infra/caddy/caddyfile_prod_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infra/caddy/caddyfile_prod_test.go b/infra/caddy/caddyfile_prod_test.go index ca7e0962..a47945bf 100644 --- a/infra/caddy/caddyfile_prod_test.go +++ b/infra/caddy/caddyfile_prod_test.go @@ -208,9 +208,13 @@ func TestProdCaddyfileRedirectsWritingArchiveBeforeDefaultProxy(t *testing.T) { "redir /writing/ https://writing.gocanto.sh/ 301", "redir /tags/* https://writing.gocanto.sh/ 301", } { - if !strings.Contains(oullinBlock, directive) { + 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/*") {