feat(session-1): socle SvelteKit et encyclopédie /zerbaj/ prérendue

SvelteKit 2 + Svelte 5 runes + TS strict, adapter-static en prérendu
intégral, trailingSlash always, paths.relative false.

Mesures de la porte de sortie
- 56 pages HTML prérendues (13 fiches × 3 langues + pages de service)
- 59 Ko de JS gzip (budget : 170) · build total 1,4 Mo (budget : 2 Mo)
- 0 chargement tiers, vérifié par grep du build
- fiche lisible sans JS : 1694 caractères de texte rendu
- npm run check : 248 fichiers, 0 erreur, 0 warning
- 0 erreur console au chargement
Lighthouse non exécuté : porte encore ouverte, à vérifier avant la S6.

Charte OKI
Tokens partagés, thème sombre par défaut, Archivo et Inter self-hébergées
(124 Ko de woff2), flag-bar à segments francs, bouton à balayage or,
gate prefers-reduced-motion unique. CSP stricte en .htaccess et _headers,
404 statique autonome, PWA avec registerSW à chemin absolu (playbook 2.9).

L'interface kréyòl n'est pas écrite par l'agent
Le catalogue gcf ne contient que les termes venant des documents du
porteur — 10 chaînes sur 132. Le reste vaut null et se replie sur le
français ; check:i18n produit la liste des 122 phrases à traduire, par
écran. <html lang> annonce la langue réellement écrite : fr tant que la
couverture reste sous 60 %, avec les fragments kréyòl marqués un par un.
Des tournures kréyòl que j'avais inventées ont été retirées.

Corrections en cours de build
- vite-plugin-pwa/sveltekit n'existe pas sans @vite-pwa/sveltekit :
  VitePWA en direct, comme le prescrivait déjà le playbook.
- @sveltejs/vite-plugin-svelte v7 n'exporte plus sveltekit : import
  depuis @sveltejs/kit/vite, vérifié sur les exports réels.
- Le sélecteur de langue fabriquait /fr/offline/, page inexistante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
OKI
2026-07-27 11:44:21 -04:00
co-authored by Claude Opus 5
parent 8cba5adfa5
commit ccad7177b3
58 changed files with 9946 additions and 3 deletions
+48
View File
@@ -0,0 +1,48 @@
# KI FWI SA YÉ ? — Apache / o2switch
# Site statique intégralement prérendu. Aucun script inline, aucun domaine tiers :
# la CSP peut donc rester stricte sans exception (playbook §2.10).
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; worker-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()"
# Le service worker ne doit jamais être servi depuis le cache HTTP,
# sinon une mise à jour du jeu peut ne jamais atteindre l'appareil.
<FilesMatch "sw\.js$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType audio/ogg "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(woff2|avif|webp|opus)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
</IfModule>
AddType application/manifest+json .webmanifest
AddType audio/ogg .opus
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# 404 statique et autonome : lisible même si tout le reste échoue.
ErrorDocument 404 /404.html
+80
View File
@@ -0,0 +1,80 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Page introuvable — KI FWI SA YÉ ?</title>
<meta name="robots" content="noindex" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<style>
/* Autonome : aucune dépendance, aucune police à charger. Cette page doit
s'afficher même quand tout le reste du site est cassé. */
:root {
--noir: #0d0d0d;
--creme: #fff8e7;
--or: #fdb813;
--vert: #00d66c;
--rouge: #ff1654;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-content: center;
gap: 1rem;
padding: 2rem;
background: var(--noir);
color: var(--creme);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
text-align: center;
}
.flag {
height: 6px;
width: 220px;
margin: 0 auto;
background: linear-gradient(
to right,
#0d0d0d 0 25%,
var(--or) 25% 50%,
var(--vert) 50% 75%,
var(--rouge) 75% 100%
);
}
h1 {
margin: 0;
font-size: clamp(1.6rem, 6vw, 2.6rem);
text-transform: uppercase;
letter-spacing: -0.01em;
}
p {
margin: 0;
opacity: 0.75;
max-width: 44ch;
}
a {
display: inline-block;
margin-top: 0.5rem;
min-height: 44px;
padding: 0.7em 1.4em;
color: var(--or);
border: 2px solid var(--or);
border-radius: 3px;
text-decoration: none;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.03em;
}
a:hover,
a:focus-visible {
background: var(--or);
color: var(--noir);
}
</style>
</head>
<body>
<div class="flag"></div>
<h1>Page introuvable</h1>
<p>Cette page nexiste pas.</p>
<a href="/">Retour à laccueil</a>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; worker-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()
/_app/immutable/*
Cache-Control: public, max-age=31536000, immutable
/fonts/*
Cache-Control: public, max-age=31536000, immutable
/sw.js
Cache-Control: no-cache
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="KI FWI SA YÉ ?">
<rect width="64" height="64" fill="#0D0D0D" />
<!-- Les quatre segments francs du drapeau panafricain, à la verticale. -->
<rect x="0" y="52" width="16" height="12" fill="#0D0D0D" />
<rect x="16" y="52" width="16" height="12" fill="#FDB813" />
<rect x="32" y="52" width="16" height="12" fill="#00D66C" />
<rect x="48" y="52" width="16" height="12" fill="#FF1654" />
<!-- Arche or, angles nets : aucune forme organique (charte §1.4). -->
<path d="M14 46V22a18 18 0 0 1 36 0v24h-9V22a9 9 0 0 0-18 0v24z" fill="#FDB813" />
<rect x="28" y="30" width="8" height="8" fill="#00D66C" />
</svg>

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"name": "KI FWI SA YÉ ?",
"short_name": "KI FWI",
"description": "Reconnaître les fruits, les arbres et les plantes des Antilles. En kréyòl d'abord.",
"lang": "gcf",
"dir": "ltr",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#0D0D0D",
"theme_color": "#0D0D0D",
"categories": ["education", "games"],
"icons": [
{ "src": "/icone-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icone-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/icone-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+14
View File
@@ -0,0 +1,14 @@
/**
* Enregistrement du service worker par fichier statique, avec chemin ABSOLU.
*
* Playbook §2.9 : l'injection du plugin n'atteint pas le HTML prérendu, et le
* registerSW.js généré utilise un chemin relatif `./sw.js` qui casse sur les routes
* imbriquées comme /fr/zerbaj/kowosol/.
*/
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function () {
/* Hors ligne au premier chargement, ou contexte non sécurisé : sans conséquence. */
});
});
}
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://ki-fwi.o-k-i.net/sitemap.xml
+19
View File
@@ -0,0 +1,19 @@
/**
* Anti-FOUC. Fichier externe et non inline : la CSP du projet est `script-src 'self'`.
* Pose les classes d'état avant le premier rendu pour éviter le flash de thème.
*/
(function () {
var racine = document.documentElement;
racine.classList.remove('no-js');
racine.classList.add('js');
try {
var choix = localStorage.getItem('oki-theme');
if (choix === 'light') racine.classList.add('light-theme');
else if (!choix && window.matchMedia('(prefers-color-scheme: light)').matches) {
racine.classList.add('light-theme');
}
} catch (e) {
/* localStorage indisponible (navigation privée stricte) : on garde le thème sombre. */
}
})();