From a7f84b4b5c95e1371c1212aee7278961a7dbbc58 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Thu, 8 Oct 2026 11:28:37 +0000 Subject: [PATCH 1/2] Send old ?locale=ru|zh page addresses to the page in that language The search engines still hold the Rails site's ?locale= addresses: on 2026-10-08 Google had 110 of openipc.org's 671 URLs with impressions carrying a query string, and /cameras/vendors/sigmastar/socs/ssc30kq?locale=ru alone earned 35 clicks in four weeks. The bundle ignores the parameter, so those readers got the English page, and its canonical told the engine to keep the English one. Only the home page's script honoured it. The seam's `location /` in both vhosts now answers a GET or HEAD with ?locale=ru or ?locale=zh, on an address without a language of its own and not a file, with a 301 to /ru/... or /zh/..., keeping the rest of the query. It applies only where the route map says page or catchall; an address the map redirects or retires is answered by the map first, and the next hop localises. Built from $request_uri and $args, never the decoded $uri. Relative, so openipc.ru's mirror rewrites it as it does every other redirect. check-config.sh --seam covers the redirects, the query handling, and the cases that must not redirect. --- deploy/nginx/check-config.sh | 25 ++++++++ deploy/nginx/conf.d/openipc-locale-param.conf | 60 +++++++++++++++++++ deploy/nginx/sites-available/org.openipc | 4 ++ deploy/nginx/sites-available/org.openipc.dev | 4 ++ 4 files changed, 93 insertions(+) create mode 100644 deploy/nginx/conf.d/openipc-locale-param.conf diff --git a/deploy/nginx/check-config.sh b/deploy/nginx/check-config.sh index 84ef86c0..23c549f9 100755 --- a/deploy/nginx/check-config.sh +++ b/deploy/nginx/check-config.sh @@ -559,6 +559,31 @@ answered GET /coupler 301 https://github.com/OpenIPC/coupler/ answered GET /wiki/some/deep/path 301 https://github.com/OpenIPC/wiki nginx answered GET /binaries 410 - nginx answered POST /binaries.json 410 - nginx +# An old `?locale=ru|zh` address goes to the page in that language, with the +# rest of its query (conf.d/openipc-locale-param.conf). Not when the address +# already names a language, not for English, not for a file, not for a write. +answered GET "/donate?locale=ru" 301 "$O/ru/donate" static +answered HEAD "/donate/?locale=zh" 301 "$O/zh/donate/" static +answered GET "/?locale=ru" 301 "$O/ru" static +answered GET "/?utm_source=x&locale=zh" 301 "$O/zh?utm_source=x" static +answered GET "/donate?locale=ru&utm_source=x" 301 "$O/ru/donate?utm_source=x" static +answered GET "/donate?a=1&locale=ru&b=2" 301 "$O/ru/donate?a=1&b=2" static +answered GET "/ru/donate?locale=ru" 200 - static +answered GET "/zh/donate?locale=ru" 404 - static +answered GET "/donate?locale=en" 200 - static +answered GET "/donate?locale=de" 200 - static +answered GET "/donate?locale=RU" 200 - static +answered GET "/favicon.png?locale=ru" 200 - static +# The route map answers its own addresses first, and the next hop localises. +answered GET "/community?locale=zh" 301 "$O/zh/community" static +# The addresses the engines actually hold, which the stub bundle does not: +# the redirect turns on the route map, not on the files. +answered GET "/cameras/vendors/sigmastar/socs/ssc30kq?locale=ru" 301 "$O/ru/cameras/vendors/sigmastar/socs/ssc30kq" static +answered GET "/cameras/vendors/goke?locale=zh" 301 "$O/zh/cameras/vendors/goke" static +answered GET "/tools/high-resolution-timer?locale=ru" 301 "$O/ru/tools/high-resolution-timer" static +answered GET "/low-latency?locale=ru" 301 "$O/ru/low-latency" static +answered GET "/cameras/boards?locale=ru" 301 "$O/ru/cameras/boards" static +answered POST "/donate?locale=ru" 405 - static answered GET /telemetry/anything 410 - nginx answered GET /zh/merchandise 410 - nginx answered GET /admin/snapshots 410 - nginx diff --git a/deploy/nginx/conf.d/openipc-locale-param.conf b/deploy/nginx/conf.d/openipc-locale-param.conf new file mode 100644 index 00000000..3ab495d1 --- /dev/null +++ b/deploy/nginx/conf.d/openipc-locale-param.conf @@ -0,0 +1,60 @@ +# `?locale=ru` and `?locale=zh` on a page, sent to the page in that language. +# +# The Rails site named the language in a parameter, and the search engines +# still hold those addresses: on 2026-10-08 Google had 110 of openipc.org's +# 671 URLs with impressions carrying a query string, and +# /cameras/vendors/sigmastar/socs/ssc30kq?locale=ru alone earned 35 clicks in +# four weeks. The bundle ignores the parameter, so every one of those readers +# got the English page, and its canonical told the engine the English page +# was the one to keep. The home page's script has always turned `/?locale=ru` +# into `/ru` (frontend/apps/site/src/pages/index.astro); this does the same +# for every page, before a byte of the wrong language is sent. +# +# Only a read, only a language the site publishes, only an address without a +# language of its own, and only a page: a name with a dot is a file. Anything +# else on the query string goes along. Behind openipc.ru the Location is +# rewritten by the mirror like every other redirect, so a reader there stays +# there. +# +# Built from $request_uri and $args, never $uri: $uri is decoded, and a +# decoded %0d%0a in a Location header is a header of the requester's choosing. +# +# The Open Wall's own `?locale=` is conf.d/openipc-wall-shell.conf's, in +# locations of its own; this is applied in the seam's `location /` only. + +# The path to localise: "/" for the home page, the path for any other page, +# and empty for an address that already names its language or is a file. +# Regexes are tried in order, so the home page is decided first. +map $request_uri $openipc_lp_path { + default ""; + "~^/(?:\?.*)?$" "/"; + "~^/(?:ru|zh)(?:[/?]|$)" ""; + "~^[^?]*\." ""; + "~^(?/[^?]*)" $openipc_lp_p; +} + +# The query string without `locale`, with its `?`, or nothing at all. +map $args $openipc_lp_args { + default "?$args"; + "" ""; + "~^locale=[^&]*$" ""; + "~^locale=[^&]*&(?.+)$" "?$openipc_lp_rest"; + "~^(?.*?)&locale=[^&]*(?&.*)?$" "?$openipc_lp_pre$openipc_lp_post"; +} + +# Only where the route map (conf.d/openipc-redirects.conf) has nothing of its +# own to say: a page, or an address nothing claims. An address it redirects or +# retires is answered by it first, and a `?locale=` the redirect carries along +# is honoured on the next request -- /about?locale=zh goes to +# /community?locale=zh and from there to /zh/community. +map $openipc_route_action $openipc_lp_routable { + default ""; + page 1; + catchall 1; +} + +map "$request_method:$openipc_lp_routable:$arg_locale:$openipc_lp_path" $openipc_locale_param_target { + default ""; + "~^(?:GET|HEAD):1:(?ru|zh):/$" "/$openipc_lp_l$openipc_lp_args"; + "~^(?:GET|HEAD):1:(?ru|zh):(?/.+)$" "/$openipc_lp_l$openipc_lp_q$openipc_lp_args"; +} diff --git a/deploy/nginx/sites-available/org.openipc b/deploy/nginx/sites-available/org.openipc index 9e5a1ab1..a9d453cb 100644 --- a/deploy/nginx/sites-available/org.openipc +++ b/deploy/nginx/sites-available/org.openipc @@ -1046,6 +1046,10 @@ server { limit_conn site_conc 10; limit_conn_status 429; + # An old `?locale=ru|zh` address goes to the page in that language + # (conf.d/openipc-locale-param.conf). + if ($openipc_locale_param_target) { return 301 $openipc_locale_param_target; } + root /srv/www/static/prod/current; try_files $uri $uri/index.html @fallback; diff --git a/deploy/nginx/sites-available/org.openipc.dev b/deploy/nginx/sites-available/org.openipc.dev index aadc1090..e21c5650 100644 --- a/deploy/nginx/sites-available/org.openipc.dev +++ b/deploy/nginx/sites-available/org.openipc.dev @@ -876,6 +876,10 @@ server { } location / { + # An old `?locale=ru|zh` address goes to the page in that language + # (conf.d/openipc-locale-param.conf). + if ($openipc_locale_param_target) { return 301 $openipc_locale_param_target; } + root /srv/www/static/dev/current; try_files $uri $uri/index.html @fallback; From 059d1a2fe502023f1f3781a0e475c428fb02552a Mon Sep 17 00:00:00 2001 From: AI Dev Date: Thu, 8 Oct 2026 11:33:19 +0000 Subject: [PATCH 2/2] Leave percent-encoded paths and repeated locales alone (Qodo on #409) --- deploy/nginx/check-config.sh | 3 +++ deploy/nginx/conf.d/openipc-locale-param.conf | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/deploy/nginx/check-config.sh b/deploy/nginx/check-config.sh index 23c549f9..db9f8886 100755 --- a/deploy/nginx/check-config.sh +++ b/deploy/nginx/check-config.sh @@ -584,6 +584,9 @@ answered GET "/tools/high-resolution-timer?locale=ru" 301 "$O/ru/tools/high-res answered GET "/low-latency?locale=ru" 301 "$O/ru/low-latency" static answered GET "/cameras/boards?locale=ru" 301 "$O/ru/cameras/boards" static answered POST "/donate?locale=ru" 405 - static +# Percent-encoded paths and repeated locales are left as they were (Qodo on #409). +answered GET "/%72u/donate?locale=zh" 200 - static +answered GET "/donate?locale=ru&locale=zh" 200 - static answered GET /telemetry/anything 410 - nginx answered GET /zh/merchandise 410 - nginx answered GET /admin/snapshots 410 - nginx diff --git a/deploy/nginx/conf.d/openipc-locale-param.conf b/deploy/nginx/conf.d/openipc-locale-param.conf index 3ab495d1..17f70bb1 100644 --- a/deploy/nginx/conf.d/openipc-locale-param.conf +++ b/deploy/nginx/conf.d/openipc-locale-param.conf @@ -23,13 +23,16 @@ # locations of its own; this is applied in the seam's `location /` only. # The path to localise: "/" for the home page, the path for any other page, -# and empty for an address that already names its language or is a file. +# and empty for an address that already names its language, is a file, or is +# percent-encoded. The route map classifies the decoded $uri, and /%72u/donate +# is /ru/donate to it; a raw path like that left alone is answered as it was. # Regexes are tried in order, so the home page is decided first. map $request_uri $openipc_lp_path { default ""; "~^/(?:\?.*)?$" "/"; "~^/(?:ru|zh)(?:[/?]|$)" ""; "~^[^?]*\." ""; + "~^[^?]*%" ""; "~^(?/[^?]*)" $openipc_lp_p; } @@ -53,8 +56,15 @@ map $openipc_route_action $openipc_lp_routable { catchall 1; } -map "$request_method:$openipc_lp_routable:$arg_locale:$openipc_lp_path" $openipc_locale_param_target { +# One `locale` only. $arg_locale is the first of several, and the others would +# ride along on the localised address with nothing left to remove them. +map $args $openipc_lp_once { + default 1; + "~(?:^|&)locale=[^&]*&(?:.*&)?locale=" ""; +} + +map "$request_method:$openipc_lp_routable$openipc_lp_once:$arg_locale:$openipc_lp_path" $openipc_locale_param_target { default ""; - "~^(?:GET|HEAD):1:(?ru|zh):/$" "/$openipc_lp_l$openipc_lp_args"; - "~^(?:GET|HEAD):1:(?ru|zh):(?/.+)$" "/$openipc_lp_l$openipc_lp_q$openipc_lp_args"; + "~^(?:GET|HEAD):11:(?ru|zh):/$" "/$openipc_lp_l$openipc_lp_args"; + "~^(?:GET|HEAD):11:(?ru|zh):(?/.+)$" "/$openipc_lp_l$openipc_lp_q$openipc_lp_args"; }