feat(lang): routage préfixé /fr/ et /en/ avec redirect JS langue sur les chemins non préfixés
- page-redirect sur / et tous les chemins sans préfixe : localStorage zektyc-lang puis navigator.language, défaut fr ; fallback FR si la page n'a pas de version EN - /en/ sert enfin la home EN (public/en/), fix de LANG_ROUTES.en - home EN générée à partir d'i18n.json (ld+json Organization/FAQPage valides) - i18n.js/components.js : switch de langue par préfixe + liens préfixés - anciennes URLs /zmap/en/* : 301 vers /en/zmap/* ; pages EN zmap déplacées vers le layout <page>/en/ standard - canonicals/hreflang fr-en-x-default sur toutes les pages statiques + sitemaps (équipe, zmap, archives, vitesses-par-route ajoutées) ; suppression du domaine obsolète zektyc.duckdns.org (robots, sitemap index, security.txt)
This commit is contained in:
parent
bf9b3ccf69
commit
370b3d8752
30 changed files with 566 additions and 124 deletions
|
|
@ -73,11 +73,11 @@ const MAX_RULES_LEN = 2000
|
|||
|
||||
const HONEYPOT_MAX_IPS = 500
|
||||
const HONEYPOT_CONFIG: SiteConfig = {
|
||||
domain: "zektyc.duckdns.org",
|
||||
domain: "riricdev.tail5ea5cd.ts.net",
|
||||
siteName: "Zektyc",
|
||||
dbName: "wordpress",
|
||||
dbUser: "wp_admin",
|
||||
adminEmail: "admin@zektyc.duckdns.org",
|
||||
adminEmail: "admin@riricdev.tail5ea5cd.ts.net",
|
||||
phpVersion: "7.4.33",
|
||||
serverSoftware: "Apache/2.4.51 (Debian)",
|
||||
serverName: "web-01",
|
||||
|
|
@ -139,6 +139,48 @@ function html(body: string, cache = "no-cache") {
|
|||
})
|
||||
}
|
||||
|
||||
function langRedirectPage(rest: string): string {
|
||||
const fr = `${SITE_URL}/fr${rest}`
|
||||
const en = `${SITE_URL}/en${rest}`
|
||||
const restJs = JSON.stringify(rest)
|
||||
return `<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<title>Zektyc</title>
|
||||
<link rel="canonical" href="${fr}" />
|
||||
<link rel="alternate" hreflang="fr" href="${fr}" />
|
||||
<link rel="alternate" hreflang="en" href="${en}" />
|
||||
<link rel="alternate" hreflang="x-default" href="${fr}" />
|
||||
<meta http-equiv="refresh" content="0; url=${fr}" />
|
||||
<script>(function(){
|
||||
var rest=${restJs};
|
||||
var target="/fr"+rest;
|
||||
try{
|
||||
var saved=localStorage.getItem("zektyc-lang");
|
||||
if(saved==="en"||saved==="fr"){ target="/"+saved+rest; }
|
||||
else {
|
||||
var ln=(navigator.language||navigator.userLanguage||"").toLowerCase();
|
||||
if(ln.indexOf("en")===0) target="/en"+rest;
|
||||
}
|
||||
}catch(e){}
|
||||
if(target.indexOf("/en")===0){
|
||||
fetch(target,{method:"HEAD",cache:"no-store"}).then(function(r){
|
||||
location.replace(r.ok?target:"/fr"+rest);
|
||||
}).catch(function(){ location.replace("/fr"+rest); });
|
||||
} else {
|
||||
location.replace(target);
|
||||
}
|
||||
})();</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirection… / Redirecting…</p>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
function bearerToken(req: Request): string | null {
|
||||
const h = req.headers.get("authorization")
|
||||
return h ? (h.match(/^Bearer\s+(\S+)$/i)?.[1] ?? null) : null
|
||||
|
|
@ -533,20 +575,20 @@ function errorPage(status: number, req?: Request): Response {
|
|||
.replace(/__MSG__/g, isFr
|
||||
? "La page que vous recherchez n\u2019existe pas, a \u00e9t\u00e9 d\u00e9plac\u00e9e ou une erreur s\u2019est produite."
|
||||
: "The page you are looking for does not exist, has been moved, or an error occurred.")
|
||||
.replace(/__HREF__/g, `/${isFr ? "" : "en/"}`)
|
||||
.replace(/__HREF__/g, `/${isFr ? "fr" : "en"}/`)
|
||||
.replace(/__BACK__/g, isFr ? "Retour \u00e0 l\u2019accueil" : "Back to home")
|
||||
.replace(/__NEWS_HREF__/g, isFr ? "blog/" : "en/blog/")
|
||||
.replace(/__NEWS_HREF__/g, isFr ? "fr/blog/" : "en/blog/")
|
||||
.replace(/__NEWS__/g, isFr ? "Blog" : "Blog")
|
||||
.replace(/__PROJECTS_HREF__/g, isFr ? "projets/" : "en/projets/")
|
||||
.replace(/__PROJECTS_HREF__/g, isFr ? "fr/projets/" : "en/projets/")
|
||||
.replace(/__PROJECTS__/g, isFr ? "Projets" : "Projects")
|
||||
.replace(/__FR_ACTIVE__/g, isFr ? "active" : "")
|
||||
.replace(/__EN_ACTIVE__/g, !isFr ? "active" : "")
|
||||
.replace(/__FR_LABEL__/g, isFr ? "<strong>FR</strong>" : "FR")
|
||||
.replace(/__EN_LABEL__/g, !isFr ? "<strong>EN</strong>" : "EN")
|
||||
.replace(/__NOTE__/g, isFr ? "Ce site ne collecte rien. Aucun cookie, aucun tracker." : "This site collects nothing. No cookies, no trackers.")
|
||||
.replace(/__TOS_HREF__/g, isFr ? "/tos" : "/en/tos")
|
||||
.replace(/__TOS_HREF__/g, isFr ? "/fr/tos" : "/en/tos")
|
||||
.replace(/__TOS__/g, isFr ? "Conditions d\u2019utilisation" : "Terms of Service")
|
||||
.replace(/__PRIV_HREF__/g, isFr ? "/privacy-policy" : "/en/privacy-policy")
|
||||
.replace(/__PRIV_HREF__/g, isFr ? "/fr/privacy-policy" : "/en/privacy-policy")
|
||||
.replace(/__PRIV__/g, isFr ? "Confidentialit\u00e9" : "Privacy")
|
||||
|
||||
return new Response(body, {
|
||||
|
|
@ -682,12 +724,16 @@ function newsIndex(lang: "fr" | "en"): string {
|
|||
|
||||
function writeSitemaps(articles: Article[]) {
|
||||
const base: { fr: string; en: string; lm: string }[] = [
|
||||
{ fr: "/fr/", en: "/en/", lm: "2026-08-14" },
|
||||
{ fr: "/fr/", en: "/en/", lm: "2026-09-06" },
|
||||
{ fr: "/fr/tos", en: "/en/tos", lm: "2026-08-14" },
|
||||
{ fr: "/fr/privacy-policy", en: "/en/privacy-policy", lm: "2026-08-14" },
|
||||
{ fr: "/fr/blog/", en: "/en/blog/", lm: "2026-08-14" },
|
||||
{ fr: "/fr/fingerprint-test", en: "/en/fingerprint-test", lm: "2026-08-16" },
|
||||
{ fr: "/fr/projets", en: "/en/projets", lm: "2026-08-17" },
|
||||
{ fr: "/fr/equipe", en: "/en/equipe", lm: "2026-08-14" },
|
||||
{ fr: "/fr/zmap", en: "/en/zmap", lm: "2026-08-17" },
|
||||
{ fr: "/fr/zmap/archives", en: "/en/zmap/archives", lm: "2026-08-17" },
|
||||
{ fr: "/fr/zmap/vitesses-par-route", en: "/en/zmap/road-speeds", lm: "2026-08-17" },
|
||||
]
|
||||
const art = articles.map((a) => ({ fr: `/fr/blog/${a.slug}/`, en: `/en/blog/${a.slug}/`, lm: a.modified || a.published }))
|
||||
const pages = [...base, ...art].sort((x, y) => x.fr.localeCompare(y.fr))
|
||||
|
|
@ -746,7 +792,7 @@ function clientIp(req: Request): string {
|
|||
|
||||
const LANG_ROUTES: Record<string, (rest: string) => string> = {
|
||||
fr: (rest) => (rest ? "/" + rest : "/"),
|
||||
en: (rest) => (rest ? "/" + rest.replace(/\/+$/, "") + "/en" : "/"),
|
||||
en: (rest) => (rest ? "/" + rest.replace(/\/+$/, "") + "/en" : "/en"),
|
||||
}
|
||||
|
||||
function resolveLangPath(path: string): string | null {
|
||||
|
|
@ -764,6 +810,13 @@ function lookup(path: string) {
|
|||
return resolved
|
||||
}
|
||||
|
||||
function isHtmlFile(path: string): boolean {
|
||||
try {
|
||||
const resolved = lookup(path)
|
||||
return !!resolved && resolved.endsWith(".html") && existsSync(resolved)
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
function matchArticle(p: string, articles: Article[]): { article: Article; lang: "fr" | "en" } | null {
|
||||
const m = p.match(/^\/blog\/([a-z0-9-]+)(?:\/en)?\/?$/)
|
||||
if (!m) return null
|
||||
|
|
@ -773,6 +826,10 @@ function matchArticle(p: string, articles: Article[]): { article: Article; lang:
|
|||
return article ? { article, lang } : null
|
||||
}
|
||||
|
||||
function artPath(matcher: { article: Article; lang: "fr" | "en" }): string {
|
||||
return `/blog/${matcher.article.slug}`
|
||||
}
|
||||
|
||||
function serveAdminIndex() {
|
||||
const f = file(normalize(join(root, "z-panel", "index.html")))
|
||||
return f.exists().then((exists) =>
|
||||
|
|
@ -1218,13 +1275,29 @@ server = serve({
|
|||
if (page) return html(page, "public, max-age=300")
|
||||
}
|
||||
|
||||
const p = resolveLangPath(pathname) ?? pathname
|
||||
// Les chemins sans préfixe de langue (/fr/ ou /en/) sont des points d'entrée :
|
||||
// on ne sert jamais de page ici, on renvoie la page-redirect qui choisit la langue
|
||||
// (localStorage puis navigateur, sinon fr). Les assets, /api, /docs et le honeypot
|
||||
// WordPress restent intacts (ils ne se résolvent pas en page HTML).
|
||||
const articles = readArticles()
|
||||
if (!/^\/(fr|en)(\/|$)/.test(pathname)) {
|
||||
// Anciennes URLs EN de l'ère « /zmap/en/... » : 301 direct vers /en/zmap/...
|
||||
const legacyEn = pathname.match(/^\/zmap\/en(?:\/(.*))?$/)
|
||||
if (legacyEn) {
|
||||
const sub = legacyEn[1] ? "/" + legacyEn[1].replace(/\/+$/, "") : ""
|
||||
return new Response(null, { status: 301, headers: { Location: `/en/zmap${sub}/` } })
|
||||
}
|
||||
const artMatch = matchArticle(pathname, articles)
|
||||
if (artMatch) return html(langRedirectPage(artPath(artMatch)), "no-cache")
|
||||
if (isHtmlFile(pathname)) return html(langRedirectPage(pathname === "/" ? "/" : pathname), "no-cache")
|
||||
}
|
||||
|
||||
const p = resolveLangPath(pathname) ?? pathname
|
||||
const matched = matchArticle(p, articles)
|
||||
if (matched) return html(articlePage(matched.article, matched.lang))
|
||||
if (p === "/actualites" || p === "/actualites/") return new Response(null, { status: 301, headers: { Location: "/blog/" } })
|
||||
if (p === "/actualites" || p === "/actualites/") return new Response(null, { status: 301, headers: { Location: "/fr/blog/" } })
|
||||
if (p === "/actualites/en" || p === "/actualites/en/") return new Response(null, { status: 301, headers: { Location: "/en/blog/" } })
|
||||
if (p.startsWith("/actualites")) return new Response(null, { status: 301, headers: { Location: "/blog/" } })
|
||||
if (p.startsWith("/actualites")) return new Response(null, { status: 301, headers: { Location: "/fr/blog/" } })
|
||||
|
||||
const filePath = lookup(decodeURIComponent(p))
|
||||
if (!filePath) return new Response("Forbidden", { status: 403 })
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const RATE_MAX = 30
|
|||
const RATE_WINDOW = 60_000
|
||||
|
||||
const UA =
|
||||
"ZMapProxy/1.0 (zektyc.duckdns.org; contact: riric65@protonmail.com; OSM proxy)"
|
||||
"ZMapProxy/1.0 (riricdev.tail5ea5cd.ts.net; contact: riric65@protonmail.com; OSM proxy)"
|
||||
|
||||
function json(data: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(data), {
|
||||
|
|
|
|||
Loading…
Reference in a new issue