diff --git a/press.config.tsx b/press.config.tsx
index 2c0e47da..85628b3d 100644
--- a/press.config.tsx
+++ b/press.config.tsx
@@ -181,6 +181,7 @@ export default defineConfig({
href="/favicon-16x16.png"
/>
+
{/* SEO / crawler hints */}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 00000000..6f4a4625
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ RustFS Documentation
+
+
+
+
+ Choose your language:
+
+
+
+
diff --git a/public/language-redirect.js b/public/language-redirect.js
new file mode 100644
index 00000000..cf6fb32c
--- /dev/null
+++ b/public/language-redirect.js
@@ -0,0 +1,65 @@
+(() => {
+ const supportedLanguages = new Set(["en", "zh", "de", "fr", "ja"]);
+ const storageKey = "rustfs-docs-language";
+ const getPathLanguage = () =>
+ window.location.pathname.match(/^\/(en|zh|de|fr|ja)(?:\/|$)/)?.[1];
+ const savePathLanguage = () => {
+ const language = getPathLanguage();
+ if (!language) return;
+
+ try {
+ window.localStorage.setItem(storageKey, language);
+ } catch {
+ // Language detection still works when storage is unavailable.
+ }
+ };
+
+ if (getPathLanguage()) {
+ savePathLanguage();
+
+ if (window.history) {
+ const pushState = window.history.pushState;
+ window.history.pushState = function (...args) {
+ const result = pushState.apply(this, args);
+ window.queueMicrotask(savePathLanguage);
+ return result;
+ };
+
+ const replaceState = window.history.replaceState;
+ window.history.replaceState = function (...args) {
+ const result = replaceState.apply(this, args);
+ window.queueMicrotask(savePathLanguage);
+ return result;
+ };
+ window.addEventListener("popstate", savePathLanguage);
+ }
+
+ return;
+ }
+
+ if (window.location.pathname !== "/" && window.location.pathname !== "/index.html") {
+ return;
+ }
+
+ let savedLanguage;
+ try {
+ savedLanguage = window.localStorage.getItem(storageKey);
+ } catch {
+ // Fall back to the browser language when storage is unavailable.
+ }
+
+ const browserLanguages = navigator.languages?.length
+ ? navigator.languages
+ : [navigator.language];
+ const detectedLanguage = browserLanguages
+ .map((language) => language?.toLowerCase().split("-")[0])
+ .find((language) => language && supportedLanguages.has(language));
+ const language =
+ savedLanguage && supportedLanguages.has(savedLanguage)
+ ? savedLanguage
+ : (detectedLanguage ?? "en");
+
+ window.location.replace(
+ `/${language}${window.location.search}${window.location.hash}`,
+ );
+})();
diff --git a/scripts/add-language-redirects.mjs b/scripts/add-language-redirects.mjs
index ec459221..3144f166 100644
--- a/scripts/add-language-redirects.mjs
+++ b/scripts/add-language-redirects.mjs
@@ -6,9 +6,12 @@ import { fileURLToPath } from 'node:url';
import { collectLanguageRedirects } from './language-redirects.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const SOURCE_INDEX = path.join(ROOT, 'public', 'index.html');
+const OUTPUT_INDEX = path.join(ROOT, 'dist', 'public', 'index.html');
const OUTPUT_REDIRECTS = path.join(ROOT, 'dist', 'public', '_redirects');
const rules = collectLanguageRedirects();
const output = [...rules].map(([source, target]) => `${source} ${target}`).join('\n');
+fs.copyFileSync(SOURCE_INDEX, OUTPUT_INDEX);
fs.writeFileSync(OUTPUT_REDIRECTS, `${output}\n`);
-console.log(`language redirects: wrote ${rules.size} Cloudflare rules`);
+console.log(`language redirects: wrote the language entry page and ${rules.size} Cloudflare rules`);
diff --git a/scripts/language-redirects.mjs b/scripts/language-redirects.mjs
index 00ca6841..8c47090f 100644
--- a/scripts/language-redirects.mjs
+++ b/scripts/language-redirects.mjs
@@ -16,7 +16,6 @@ function prefixEnglishTarget(target) {
export function collectLanguageRedirects() {
/** @type {Map} */
const rules = new Map();
- rules.set('/', '/en 302');
const existingRules = fs.readFileSync(SOURCE_REDIRECTS, 'utf8').split('\n');
for (const line of existingRules) {