6767 }
6868}
6969
70+ # Compose one branded error page (beta): when the origin self-describes
71+ # an error (X-Error-Code) and the client meaningfully asked for HTML,
72+ # swap the body for the Next error page while KEEPING the origin status
73+ # (the inner proxy would otherwise answer with Next's 200). JSON clients
74+ # and Accept:*/* scanner probes get the origin response untouched —
75+ # composition costs a Next render. args[0] = the status code.
76+ (compose_beta_err) {
77+ @err{args[0]} {
78+ status {args[0]}
79+ header X-Error-Code *
80+ }
81+ handle_response @err{args[0]} {
82+ @html{args[0]} {
83+ header Accept *text /html*
84+ method GET HEAD
85+ }
86+ handle @html{args[0]} {
87+ rewrite * /_error/ {args[0]} ?code={rp.header.X-Error-Code} &from={uri}
88+ reverse_proxy spoo_next_beta:3000 {
89+ @ok{args[0]} status 2xx
90+ replace_status @ok{args[0]} {args[0]}
91+ header_up Host {host}
92+ }
93+ }
94+ handle {
95+ copy_response
96+ }
97+ }
98+ }
99+
70100spoo.me, www.spoo.me {
71101 import common_tls
72102 reverse_proxy app:8000 {
@@ -80,9 +110,88 @@ spoo.me, www.spoo.me {
80110# container name, not compose service name.
81111beta.spoo.me {
82112 import common_tls
113+
114+ # BETA-ONLY (do not port to prod): beta is a duplicate of the whole
115+ # site — never let crawlers index it. Wins over the matchers below
116+ # because handle is ordered before reverse_proxy.
117+ @robots path /robots.txt
118+ handle @robots {
119+ header Content-Type "text/plain; charset=utf-8"
120+ respond "User-agent: *
121+ Disallow: /" 200
122+ }
123+
124+ # Only paths with a built Next page belong here — a listed path
125+ # with no page behind it 404s in Next instead of falling through
126+ # to FastAPI.
127+ @next {
128+ path /about /about/*
129+ path /pricing /pricing/*
130+ path /apps /apps/*
131+ path /testimonials /testimonials/*
132+ path /login /signup /forgot-password
133+ path /onboarding /onboarding/*
134+ path /privacy /terms /legal
135+ path /relay/*
136+ path /dashboard /dashboard/*
137+ # Composed-error URL (Next rewrites it to error-pages/ internally).
138+ path /_error/*
139+ path /_next/* /icon.png
140+ path /favicon.ico /favicon.png /favicon.svg
141+ # Next public/ asset dirs + the favicon-proxy route. A new top-level
142+ # dir under public/ needs a line here AND a reserved-aliases entry,
143+ # or it 404s through FastAPI and takes client pages down with it.
144+ path /geo/* /brand/* /icons-3d/*
145+ path /api/favicon
146+ # SEO files (robots.ts/sitemap.ts generators + public/ text files).
147+ # Exact .well-known path only — the rest of that namespace stays
148+ # with the backend for whatever claims it later.
149+ path /robots.txt /sitemap.xml /humans.txt
150+ path /security.txt /.well-known/security.txt
151+ }
152+ reverse_proxy @next spoo_next_beta:3000 {
153+ header_up X-Real-IP {client_ip}
154+ header_up X-Forwarded-For {client_ip}
155+ header_up Host {host}
156+ }
157+
158+ # Paths whose GET is a Next page but whose POST is a shipped backend
159+ # contract: / (the original anonymous shorten API), /contact and
160+ # /report (legacy form intakes), /stats (legacy lookup form) and
161+ # /stats/{code} (the legacy public JSON API). Next serves pages to
162+ # POSTs with a 200, so an unscoped matcher silently breaks every one
163+ # of these for API clients — caught twice on beta.
164+ @next_get {
165+ method GET HEAD
166+ path / /contact /report
167+ path /stats /stats/*
168+ }
169+ reverse_proxy @next_get spoo_next_beta:3000 {
170+ header_up X-Real-IP {client_ip}
171+ header_up X-Forwarded-For {client_ip}
172+ header_up Host {host}
173+ }
174+
175+ # /{code}+ preview — any single segment ending in + (matched against
176+ # the DECODED path, so emoji aliases land here too). No valid alias
177+ # contains +, so this can never shadow a real code.
178+ @preview path_regexp ^/[^/]+\+$
179+ reverse_proxy @preview spoo_next_beta:3000 {
180+ header_up X-Real-IP {client_ip}
181+ header_up X-Forwarded-For {client_ip}
182+ header_up Host {host}
183+ }
184+
185+ # Everything else: short codes, /auth/*, /oauth/*, /api/v1/*, /static/*
83186 reverse_proxy spoo_app_beta:8000 {
84187 header_up X-Real-IP {client_ip}
85188 header_up X-Forwarded-For {client_ip}
189+
190+ import compose_beta_err 404
191+ import compose_beta_err 410
192+ import compose_beta_err 429
193+ import compose_beta_err 451
194+ import compose_beta_err 500
86195 }
87196}
88197
0 commit comments