feat: easter egg théière 418 + Konami obfusqué via global.js centralisé

- /418/ (et /fr|en/418) : vraie page HTTP 418 'Je suis une théière', SVG
  animé (vapeur), boutons café (refus RFC 2324) / thé (toast), noindex.
- Nouveau public/js/global.js chargé sur toutes les pages (statiques,
  dynamiques articles/blog, page d'erreur) qui :
  * écoute le code Konami ↑↑↓↓←→←→BA → redirige vers /418/ ;
  * charge ses clés par codes de caractères (aucune chaîne révélatrice en
    clair : ni 'ArrowUp', ni '/418/', ni 'b/a' dans la source) ;
  * sert de dispatcher centralisé : injecte par page (components/discord/
    zfprobe) via <body data-page=...> ou règles de chemin, sans doublon.
- easter.js supprimé au profit de global.js unique sur 22 pages statiques.
This commit is contained in:
riricdev 2026-09-07 15:16:20 +02:00
commit 1482c6aaf0
24 changed files with 217 additions and 0 deletions

View file

@ -47,6 +47,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -47,6 +47,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -121,6 +121,7 @@
<link rel="me" href="https://piaille.fr/@zektyc" /> <link rel="me" href="https://piaille.fr/@zektyc" />
<link rel="stylesheet" href="/css/style.css?v=8" /> <link rel="stylesheet" href="/css/style.css?v=8" />
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
<script src="/js/i18n.js?v=3" defer></script> <script src="/js/i18n.js?v=3" defer></script>
<script src="/js/discord.js?v=1" defer></script> <script src="/js/discord.js?v=1" defer></script>

View file

@ -57,6 +57,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -57,6 +57,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -10,6 +10,7 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=8" /> <link rel="stylesheet" href="/css/style.css?v=8" />
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/i18n.js" defer></script> <script src="/js/i18n.js" defer></script>
<script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script> <script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script>
<style> <style>

View file

@ -161,6 +161,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
<script src="/js/zfprobe.js?v=15" defer></script> <script src="/js/zfprobe.js?v=15" defer></script>
</body> </body>

View file

@ -159,6 +159,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
<script src="/js/zfprobe.js?v=15" defer></script> <script src="/js/zfprobe.js?v=15" defer></script>
</body> </body>

View file

@ -124,6 +124,7 @@
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
<script src="/js/i18n.js?v=3" defer></script> <script src="/js/i18n.js?v=3" defer></script>
<script src="/js/discord.js?v=1" defer></script> <script src="/js/discord.js?v=1" defer></script>
<script src="/js/global.js?v=1" defer></script>
</head> </head>
<body> <body>
<header id="h"></header> <header id="h"></header>

81
public/js/global.js Normal file
View file

@ -0,0 +1,81 @@
(function (w, d) {
"use strict";
function dc() {
var a = [].slice.call(arguments), o = "";
for (var i = 0; i < a.length; i++) o += String.fromCharCode(a[i]);
return o;
}
function bt() {
var p = dc(65, 114, 114, 111, 119);
var u = p + dc(85, 112);
var dn = p + dc(68, 111, 119, 110);
var lf = p + dc(76, 101, 102, 116);
var rt = p + dc(82, 105, 103, 104, 116);
var b = String.fromCharCode(98);
var A = String.fromCharCode(97);
return [u, u, dn, dn, lf, rt, lf, rt, b, A];
}
var _s = null;
function seq() { if (!_s) _s = bt(); return _s; }
var dst = dc(47, 52, 49, 56, 47);
var idx = 0, timer = null;
function reset() { idx = 0; if (timer) { clearTimeout(timer); timer = null; } }
function notField(t) {
if (!t) return false;
var g = (t.tagName || "").toUpperCase();
return g === "INPUT" || g === "TEXTAREA" || g === "SELECT" || t.isContentEditable;
}
w.addEventListener("keydown", function (e) {
if (notField(e.target)) return;
if (e.ctrlKey || e.metaKey || e.altKey) { reset(); return; }
var k = e.key;
if (k && k.length === 1) k = k.toLowerCase();
var s = seq();
if (k === s[idx]) {
idx++;
if (timer) clearTimeout(timer);
timer = setTimeout(reset, 4000);
if (idx === s.length) {
reset();
var a = d.createElement("a");
a.href = encodeURI(dst);
a.rel = "noreferrer";
w.location.assign(a.href);
}
} else reset();
});
var pathname = w.location.pathname || "/";
function isHome() {
return pathname === "/" || pathname === "/index.html"
|| pathname === "/fr/" || pathname === "/fr/index.html"
|| pathname === "/en/" || pathname === "/en/index.html";
}
function isFp() { return pathname.indexOf(dc(102, 105, 110, 103, 101, 114, 112, 114, 105, 110, 116)) !== -1; }
function shouldLoad(name) {
var at = (d.body && d.body.getAttribute("data-page")) || "";
if (at === name) return true;
var rules = { discord: isHome(), zfprobe: isFp(), components: true };
return !!rules[name];
}
function loadScript(name) {
if (!shouldLoad(name)) return;
var needle = "/js/" + name + ".js";
if (d.querySelector('script[src*="' + needle + '"]')) return;
var s = d.createElement("script");
s.src = needle + dc(63, 118, 61, 49);
s.defer = true;
d.head.appendChild(s);
}
(function () {
var want = ["components", "discord", "zfprobe"];
for (var i = 0; i < want.length; i++) loadScript(want[i]);
})();
})(window, document);

View file

@ -210,6 +210,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -210,6 +210,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -73,6 +73,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -73,6 +73,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -236,6 +236,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -236,6 +236,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -61,6 +61,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -106,6 +106,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -106,6 +106,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -121,6 +121,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -127,6 +127,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -202,6 +202,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -210,6 +210,7 @@
<footer id="f"></footer> <footer id="f"></footer>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script> <script src="/js/components.js?v=6" defer></script>
</body> </body>
</html> </html>

View file

@ -578,12 +578,114 @@ const ERROR_MESSAGES: Record<number, { fr: string; en: string }> = {
405: { fr: "M\u00e9thode non autoris\u00e9e", en: "Method Not Allowed" }, 405: { fr: "M\u00e9thode non autoris\u00e9e", en: "Method Not Allowed" },
408: { fr: "D\u00e9lai d\u2019attente d\u00e9pass\u00e9", en: "Request Timeout" }, 408: { fr: "D\u00e9lai d\u2019attente d\u00e9pass\u00e9", en: "Request Timeout" },
413: { fr: "Requ\u00eate trop volumineuse", en: "Payload Too Large" }, 413: { fr: "Requ\u00eate trop volumineuse", en: "Payload Too Large" },
418: { fr: "Je suis une th\u00e9i\u00e8re", en: "I'm a Teapot" },
429: { fr: "Trop de requ\u00eates", en: "Too Many Requests" }, 429: { fr: "Trop de requ\u00eates", en: "Too Many Requests" },
500: { fr: "Erreur interne du serveur", en: "Internal Server Error" }, 500: { fr: "Erreur interne du serveur", en: "Internal Server Error" },
502: { fr: "Passerelle incorrecte", en: "Bad Gateway" }, 502: { fr: "Passerelle incorrecte", en: "Bad Gateway" },
503: { fr: "Service indisponible", en: "Service Unavailable" }, 503: { fr: "Service indisponible", en: "Service Unavailable" },
} }
function teapotPage(lang: "fr" | "en"): string {
const isFr = lang === "fr"
const title = isFr ? "Je suis une th\u00e9i\u00e8re" : "I'm a Teapot"
const sub = isFr
? "Court et trapu, c\u2019est le serveur. Il refuse poliment d\u2019infuser le caf\u00e9 (RFC 2324)."
: "Short and stout, it\u2019s the server. It politely refuses to brew coffee (RFC 2324)."
const text = isFr
? "Le caf\u00e9, c\u2019est non. Le th\u00e9, en revanche, c\u2019est toujours oui \u2014 le serveur est un expert incontest\u00e9."
: "Coffee is a no. Tea, on the other hand, is always a yes \u2014 the server is an undisputed expert."
const coffee = isFr ? "Commander un caf\u00e9" : "Order a coffee"
const tea = isFr ? "Servir le th\u00e9" : "Serve some tea"
const coffeeToast = isFr ? "418 \u00b7 Je suis une th\u00e9i\u00e8re \u2014 trop court et trapu pour le caf\u00e9." : "418 \u00b7 I'm a teapot \u2014 too short and stout for coffee."
const teaToast = isFr ? "De l\u2019eau, des feuilles, un peu de patience \u2014 et c\u2019est parti !" : "Water, leaves, a little patience \u2014 and away we go!"
const hint = isFr ? "Au fait : si vous l\u2019avez trouv\u00e9e par hasard, c\u2019est que la s\u00e9quence secr\u00e8te fonctionne." : "By the way: if you found this by chance, the secret sequence works."
const note = isFr ? "La vie priv\u00e9e n\u2019est pas un luxe \u2014 mais le caf\u00e9, ici, l\u2019est." : "Privacy isn\u2019t a luxury \u2014 but coffee, here, is."
return `<!doctype html>
<html lang="${lang}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>${title} · Zektyc</title>
<meta name="theme-color" content="#070a0f" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=8" />
<script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<style>
.tea-hero{text-align:center;padding:3rem 1rem 4rem}
.tea-hero h1{font-size:clamp(5rem,18vw,11rem);font-weight:900;line-height:1;letter-spacing:-0.04em;margin-bottom:.75rem}
.tea-hero .tea-sub{color:var(--text-muted);font-size:1.05rem;max-width:44ch;margin:0 auto 1.25rem;line-height:1.6}
.tea-stage{position:relative;width:min(340px,80vw);margin:1.5rem auto 2rem}
.tea-pot{display:block;width:100%;color:var(--text)}
.tea-steam{position:absolute;pointer-events:none}
.tea-steam span{position:absolute;bottom:0;display:block;width:14px;height:38px;border-radius:999px;background:radial-gradient(circle at 50% 40%,rgba(var(--accent-rgb,255,255,255),.35),transparent 70%);animation:tea-rise 3s ease-in infinite}
.tea-steam .ts1{left:44%;animation-delay:0s}
.tea-steam .ts2{left:52%;animation-delay:1s;transform:scaleX(-1)}
.tea-steam .ts3{left:60%;animation-delay:2s}
@keyframes tea-rise{0%{opacity:0;transform:translateY(10px) scale(.7)}30%{opacity:1}100%{opacity:0;transform:translateY(-120px) scale(1.15)}}
@media (prefers-reduced-motion: reduce){.tea-steam span{animation:none;opacity:.35}}
.tea-actions{display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
.tea-hint{color:var(--text-muted);opacity:.7;font-size:.85rem;margin-top:2.25rem}
.tea-note{color:var(--text-muted);font-size:.9rem;letter-spacing:.02em}
.tea-toast{position:fixed;left:50%;bottom:2rem;transform:translateX(-50%);background:var(--bg-elevated,#111);border:1px solid var(--border,#333);color:var(--text);padding:.8rem 1.2rem;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.35);opacity:0;transition:opacity .25s ease;z-index:50;pointer-events:none;max-width:90vw;text-align:center}
.tea-toast.show{opacity:1}
</style>
</head>
<body>
${siteHeader(lang)}
<main class="site-main" id="main">
<section class="tea-hero">
<h1>418</h1>
<p class="tea-sub">${sub}</p>
<div class="tea-stage" aria-hidden="true">
<div class="tea-steam"><span class="ts1"></span><span class="ts2"></span><span class="ts3"></span></div>
<svg class="tea-pot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 130" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
<path d="M58 42h54" />
<path d="M85 42v-9a7 7 0 0 0-7-7h-7a7 7 0 0 0-7 7v9" />
<path d="M30 50l9 52a21 21 0 0 0 21 19h50a21 21 0 0 0 21-19l9-52" />
<path d="M39 121v5a6 6 0 0 0 6 6h55a6 6 0 0 0 6-6v-5" />
<path d="M32 60c-23 2-25 40-3 46" />
<path d="M140 64c25-4 25 26 0 32" />
</svg>
</div>
<p>${text}</p>
<div class="tea-actions">
<button type="button" class="btn btn-primary" data-tea="coffee">${coffee}</button>
<button type="button" class="btn" data-tea="tea">${tea}</button>
</div>
<p class="tea-hint">\u2191 \u2191 \u2193 \u2193 \u2190 \u2192 \u2190 \u2192 B A</p>
<p class="tea-note">${note}</p>
</section>
</main>
<div class="tea-toast" role="status" aria-live="polite"></div>
${siteFooter(lang)}
<script>
(function () {
var toast = document.querySelector(".tea-toast");
var spin = function (m) {
toast.textContent = m;
toast.classList.add("show");
clearTimeout(toast._t);
toast._t = setTimeout(function () { toast.classList.remove("show"); }, 3200);
};
document.querySelectorAll("[data-tea]").forEach(function (b) {
b.addEventListener("click", function () {
spin(b.getAttribute("data-tea") === "coffee" ? "${coffeeToast}" : "${teaToast}");
});
});
})();
</script>
</body>
</html>`
}
function errorPage(status: number, req?: Request): Response { function errorPage(status: number, req?: Request): Response {
const urlPath = req ? new URL(req.url).pathname : "/" const urlPath = req ? new URL(req.url).pathname : "/"
const lang: "fr" | "en" = /^\/en(\/|$)/.test(urlPath) ? "en" : "fr" const lang: "fr" | "en" = /^\/en(\/|$)/.test(urlPath) ? "en" : "fr"
@ -677,6 +779,7 @@ ${ldJson}
</script> </script>
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/i18n.js" defer></script> <script src="/js/i18n.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script> <script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script>
</head> </head>
<body> <body>
@ -737,6 +840,7 @@ function newsIndex(lang: "fr" | "en"): string {
<link rel="stylesheet" href="/css/style.css?v=7" /> <link rel="stylesheet" href="/css/style.css?v=7" />
<script src="/js/theme.js" defer></script> <script src="/js/theme.js" defer></script>
<script src="/js/i18n.js" defer></script> <script src="/js/i18n.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script> <script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script>
</head> </head>
<body> <body>
@ -1316,6 +1420,16 @@ server = serve({
return new Response(body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" } }) return new Response(body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" } })
} }
// Œuf de Pâques : la théière (RFC 2324). Accessible par la séquence Konami.
const teaMatch = pathname.match(/^\/(?:418|(fr|en)\/418)(?:\/)?$/)
if (teaMatch) {
const lang: "fr" | "en" = teaMatch[1] === "en" ? "en" : detectLang(req)
return new Response(teapotPage(lang), {
status: 418,
headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" },
})
}
// Pages de documentation globale des API // Pages de documentation globale des API
const docsMatch = pathname.match(/^\/docs\/([a-z0-9-]+)\/?$/) const docsMatch = pathname.match(/^\/docs\/([a-z0-9-]+)\/?$/)
if (docsMatch) { if (docsMatch) {