docs+fix: nettoyage post-migration et conformité svelte-autofixer

- CONTRIBUTING.md et SEO.md réécrits pour la structure SvelteKit
- README : section documents de référence (charte, recette, AGENTS.md, guides Svelte 5)
- Liens internes via resolve()/asset() de $app/paths, externes en rel="external"
- Seo.svelte : JSON-LD construits dans le script (</script> échappé)
- Titres de sections : {' '} remplacé par &#32;
- Renommage svelte_core_bestpractices.md
- Passe svelte-autofixer : 0 issue restante hors {@html} first-party assumés
This commit is contained in:
sucupira
2026-07-21 08:41:55 -04:00
parent d0d80a105e
commit 30858926fa
28 changed files with 217 additions and 277 deletions
+65 -100
View File
@@ -2,11 +2,15 @@
Merci de votre intérêt pour contribuer au site de l'ORGANISATION KA INTERNATIONALE !
Le site est une application **SvelteKit + Svelte 5 (runes) + TypeScript**, entièrement pré-rendue en statique. Avant de contribuer, lisez `charte-oki-design-system.md` (marque) et `svelte_core_bestpractices.md` (conventions Svelte 5).
## Ajouter du contenu
Le contenu éditorial vit dans les bundles i18n : `src/lib/i18n/fr/*.json` et `src/lib/i18n/en/*.json`**toujours modifier les deux locales**.
### Ajouter un service hébergé
Éditez `src/_data/i18n/fr/services.json` (et la version `en/`) :
Éditez `src/lib/i18n/fr/services.json` (et `en/`) :
```json
{
@@ -18,122 +22,85 @@ Merci de votre intérêt pour contribuer au site de l'ORGANISATION KA INTERNATIO
}
```
Le service apparaîtra automatiquement dans la grille de fallback du village (`HostingVillage.svelte`). Pour qu'il ait aussi son bâtiment dans la scène SVG, voir « Modifier un composant ».
### Ajouter un projet web
Éditez `src/_data/i18n/fr/projects.json` (et la version `en/`) :
```json
{
"name": "NOM.PROJET",
"icon": "📱",
"domain": "nom.projet",
"description": "Description du projet",
"url": "https://nom.projet/"
}
```
Éditez `src/lib/i18n/fr/projects.json` (et `en/`). Le champ `icon` attend un emoji legacy qui sera remplacé au rendu par un picto du sprite — voir le mapping dans `src/lib/components/sections/pictos.ts` et choisissez-en un qui y figure.
### Ajouter un client ou partenaire
Pour les logos, privilégiez une image locale dans `src/assets/images/` et utilisez le filtre `| cdn` dans les templates. Les URLs distantes fonctionnent mais ne passent pas par le CDN.
Placez le logo dans `src/lib/assets/images/` (et `clients/` pour les clients), puis importez-le dans le composant de section concerné via vite-imagetools :
```ts
import logo from '$lib/assets/images/mon-logo.png?format=avif;webp;png&w=320;640&as=picture';
```
et affichez-le avec `<ResponsiveImage picture={logo} alt="…" />`. Ne jamais référencer une image distante : tout asset est self-hébergé (doctrine souveraineté + CSP).
### Ajouter une valeur
Éditez `src/_data/i18n/fr/values.json` (et la version `en/`) :
```json
{
"icon": "🌟",
"title": "Titre",
"description": "Description de la valeur"
}
```
Éditez `src/lib/i18n/fr/values.json` (et `en/`). Le champ `icon` suit le même mapping que les projets (`pictos.ts`).
### Modifier la navigation
Éditez `src/_data/i18n/fr/navigation.json` (et la version `en/`) :
Éditez `src/lib/i18n/fr/navigation.json` (et `en/`) :
```json
{
"text": "Texte du lien",
"url": "#section",
"external": false
}
{ "text": "Texte du lien", "url": "/#ancre", "external": false }
```
Pour un lien externe, mettez `"external": true`.
Pour un lien externe, mettez `"external": true`. Les URLs de la version EN doivent pointer sous `/en/`.
### Modifier les informations de contact
Les liens de contact sont regroupés par section dans `src/_data/i18n/fr/contact.json` (et la version `en/`). L'icône affichée est déterminée automatiquement à partir du texte du lien (`whatsapp`, `telegram`, `discord`, `labola`, `kontak`/`mail`).
```json
{
"title": "💬 Communauté",
"links": [
{ "text": "Discord", "url": "https://discord.gg/DPvUCA8dD" }
]
}
```
Les liens de contact sont regroupés dans `src/lib/i18n/fr/contact.json` (et `en/`). L'icône de chaque lien est déduite de son libellé (`whatsapp`, `telegram`, `discord`, `labola`, `kontak`/`mail`). Les emojis en tête des titres de groupes sont convertis en pictos du sprite (`pictos.ts`).
## Modifier les styles
Les styles sont dans `src/assets/css/styles.css`.
### Variables CSS principales
```css
--or-oki: #E8A625;
--noir-oki: #000000;
--vert-oki: #00A854;
--rouge-oki: #ED1C4C;
--blanc-creme: #F7F7F2;
--noir-profond: #1A1A1A;
--gris-sombre: #2A2A2A;
--or-clair: #F5BD4F;
--vert-clair: #00C569;
--rouge-clair: #FF2D5F;
```
- **Tokens (couleurs, typo, espacements, motion)** : `src/lib/styles/oki-tokens.css` — ne jamais recopier une valeur de la charte à la main, utiliser les variables.
- **Primitives partagées** (boutons, cartes, tags, flag-bar, gate reduced-motion) : `src/lib/styles/base.css`.
- **Styles d'une section** : dans le `<style>` scopé de son composant (`src/lib/components/sections/`).
- Durées et courbes d'animation : toujours via les tokens (`--dur-tanbou`, `--ease-ka`…), jamais en dur. On n'anime que `transform` et `opacity`.
- Le thème clair est géré par surcharge des tokens (`html.light-theme`) — ne pas écrire de sélecteur `html.light-theme` par composant.
## Créer une nouvelle page
1. Créez un fichier `.njk` dans `src/` :
1. Créez `src/routes/ma-page/+page.svelte` (et sa contrepartie `src/routes/en/my-page/+page.svelte` si bilingue) :
```njk
---
layout: layouts/base.njk
title: Titre de la page
description: Description pour le SEO
---
```svelte
<script lang="ts">
import Seo from '$lib/components/Seo.svelte';
import { getBundle } from '$lib/i18n';
<section>
<h1>{{ title }}</h1>
<p>Contenu de votre page</p>
const t = getBundle('fr');
</script>
<Seo title="Titre de la page" description="Description pour le SEO" path="/ma-page/" locale="fr" />
<section class="section">
<div class="container">
<h1>Titre de la page</h1>
</div>
</section>
```
2. Ajoutez le lien dans `src/_data/i18n/fr/navigation.json` (et `en/`) si nécessaire.
2. Ajoutez la correspondance FR↔EN dans `PATH_PAIRS` (`src/lib/i18n/index.ts`) pour l'hreflang et la bascule de langue.
3. Ajoutez l'URL dans `static/sitemap.xml` et, si nécessaire, dans `navigation.json`.
4. Vérifiez : `npm run check && npm run build`.
3. Mettez à jour `src/sitemap.xml`.
## Modifier un composant
4. Reconstruisez le site : `npm run build`.
- Sections de l'accueil : `src/lib/components/sections/`
- Layout : `Nav.svelte`, `Footer.svelte`, `Seo.svelte`
- Motion : `src/lib/components/motion/` (KineticText, ScrollProgressBar)
- Icônes : sprite `static/icons.svg` (set OKI, charte §4) et `src/lib/components/icons/BrandIcon.svelte` (logos de marques)
## Modifier un partial
Après toute modification d'un composant, passez l'autofixer (voir `svelte-5_code_writer.md`) :
Les partials sont dans `src/_includes/partials/` :
- `nav.njk` - Navigation
- `hero.njk` - Section hero (accueil)
- `services.njk` - Services hébergés
- `projects.njk` - Projets web
- `mission.njk` - Mission et valeurs
- `hosting.njk` - Services d'hébergement
- `support.njk` - Section dons
- `clients.njk` - Clients / organisations ayant fait confiance
- `partners.njk` - Partenaires
- `faq.njk` - Questions fréquentes
- `engagement.njk` - Appels à l'action
- `contact.njk` - Informations de contact
- `footer.njk` - Pied de page
```bash
npx @sveltejs/mcp svelte-autofixer ./src/lib/components/sections/MonComposant.svelte
```
## Workflow de développement
@@ -146,12 +113,12 @@ Les partials sont dans `src/_includes/partials/` :
3. Testez localement :
```bash
npm start
npm run dev
```
4. Buildez pour vérifier :
4. Vérifiez types, accessibilité et build :
```bash
npm run build
npm run check && npm run build && npm run preview
```
5. Commitez vos changements :
@@ -167,24 +134,22 @@ Les partials sont dans `src/_includes/partials/` :
## Standards de code
### HTML/Nunjucks
- Indentation : 4 espaces
- Toujours fermer les balises
- Attributs entre guillemets doubles
### Svelte / TypeScript
- Svelte 5 runes uniquement (`$state`, `$derived`, `$props`…) — voir `svelte_core_bestpractices.md`
- TypeScript strict, props typées
- `onclick={...}` (jamais `on:click`), blocs `{#each}` à clé unique
- `svelte-autofixer` propre sur tout composant modifié, `npm run check` à zéro warning
### CSS
- Indentation : 4 espaces
- Utiliser les variables CSS quand possible
- Mobile-first pour le responsive
- Indentation : tabulations (convention du projet)
- Variables des tokens quand possible, mobile-first
- Angles nets : `border-radius` ≤ `var(--radius-md)` (6 px)
### JavaScript
- Indentation : 4 espaces
- Vanilla JS (pas de framework)
- Commentaires en français
- Gestion des erreurs
### JavaScript (fichiers statiques)
- Vanilla JS, commentaires en français, aucun script inline dans le HTML (CSP `script-src 'self'`)
### Sécurité
- Ne pas ajouter de scripts inline sans ajuster la CSP dans `src/.htaccess`.
- Ne pas ajouter de ressource externe (font, CDN, script) : tout se self-héberge. Si une exception est indispensable, ajuster la CSP dans `static/.htaccess` **et** `static/_headers`.
- Conserver les headers de sécurité lors d'une modification du serveur.
## Questions ?
+9 -2
View File
@@ -61,9 +61,16 @@ FR par défaut, EN sous `/en/`. Les bundles JSON de `src/lib/i18n/{fr,en}/` alim
Headers de sécurité (CSP stricte `default-src 'self'`, HSTS, `X-Frame-Options: DENY`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`) livrés en deux formats : `static/.htaccess` (Apache/o2switch) et `static/_headers` (Cloudflare Pages). Voir **DEPLOY.md**.
## Charte
## Charte et documents de référence
Couleurs, typographie, motion (cadences gwoka), iconographie SVG (zéro emoji en interface) : voir `charte-oki-design-system.md`. Thème sombre par défaut, thème clair en opt-in persisté (`localStorage['oki-theme']`).
- **`charte-oki-design-system.md`** — marque canonique : couleurs, typographie, motion (cadences gwoka), iconographie SVG (zéro emoji en interface), voix. Prime pour tout ce qui est marque.
- **`recette-sveltekit-playbook-agent.md`** — méthode technique : principes, budgets performance, règles motion, DoD (sa version longue : `recette-sveltekit-15-sites-awwwards.md`).
- **`AGENTS.md`** — point d'entrée pour les agents de code : impose l'usage de l'outillage Svelte MCP.
- **`svelte-5_code_writer.md`** — commandes `@sveltejs/mcp` (doc Svelte 5 + `svelte-autofixer`, à passer sur tout composant modifié).
- **`svelte_core_bestpractices.md`** — conventions runes Svelte 5 ($state/$derived/$effect, events, snippets, styles).
- **`DEPLOY.md`** — déploiement o2switch (Apache) et Cloudflare Pages.
Thème sombre par défaut, thème clair en opt-in persisté (`localStorage['oki-theme']`).
## Licence
+61 -119
View File
@@ -1,11 +1,11 @@
# Guide SEO - o-k-i.net
Ce document décrit l'optimisation SEO mise en place sur le site o-k-i.net V2.
Ce document décrit l'optimisation SEO du site o-k-i.net (V3, SvelteKit statique).
## 📋 Fichiers SEO
### 1. robots.txt
**Emplacement :** `src/robots.txt`
**Emplacement :** `static/robots.txt` (servi à `/robots.txt`)
Permet aux moteurs de recherche d'indexer tout le contenu du site et indique l'emplacement du sitemap.
@@ -16,174 +16,116 @@ Sitemap: https://o-k-i.net/sitemap.xml
```
### 2. sitemap.xml
**Emplacement :** `src/sitemap.xml`
**Emplacement :** `static/sitemap.xml` (servi à `/sitemap.xml`)
Liste toutes les pages du site avec leurs priorités et fréquences de mise à jour.
Liste les 8 pages du site avec priorités et fréquences :
**Pages incluses :**
- Page d'accueil (priorité 1.0, hebdomadaire)
- Mentions légales (priorité 0.3, mensuelle)
- `/` et `/en/` (priorité 1.0, hebdomadaire)
- `/dons/` et `/en/donate/` (0.6, mensuelle)
- `/presse/` et `/en/press/` (0.5, mensuelle)
- `/mentions-legales/` et `/en/legal-notice/` (0.3, mensuelle)
**À mettre à jour :** Lors de l'ajout de nouvelles pages.
**À mettre à jour :** lors de l'ajout de nouvelles pages.
## 🏷️ Meta Tags
### Meta Tags de base
Définis dans `src/_includes/layouts/base.njk` :
Tous les meta tags sont centralisés dans le composant **`src/lib/components/Seo.svelte`**, alimenté par `src/lib/i18n/{locale}/site.json`. Chaque page l'invoque avec ses props :
- **Title** : Dynamique selon la page
- **Description** : Personnalisable par page via front matter
- **Keywords** : OKI, Fédiverse, PeerTube, Pleroma, Funkwhale, Castopod, Guadeloupe, logiciel libre, GAFAM
- **Author** : Organisation Ka Internationale
- **Canonical URL** : Évite le contenu dupliqué
```svelte
<Seo title="Titre de la page" description="Description spécifique" path="/ma-page/" locale="fr" />
```
### Open Graph (Facebook, LinkedIn)
- **Title** : `Titre | ORGANISATION KA INTERNATIONALE` (home : `… | OKI`)
- **Description** : par page, sinon `site.description`
- **Keywords** : `site.keywords` · **Author** : `site.name`
- **Canonical URL** : `https://o-k-i.net{path}` — évite le contenu dupliqué
- **Hreflang** : `fr`, `en`, `x-default`, calculés via `alternatePath()` (`src/lib/i18n/index.ts`)
### Open Graph (10 balises)
```html
<meta property="og:type" content="website">
<meta property="og:url" content="https://o-k-i.net">
<meta property="og:title" content="...">
<meta property="og:description" content="...">
<meta property="og:image" content="https://o-k-i.net/assets/images/logo-512x512.png">
<meta property="og:url" content="https://o-k-i.net/…">
<meta property="og:title" content="">
<meta property="og:description" content="">
<meta property="og:image" content="https://o-k-i.net/images/logo-512x512.png">
<meta property="og:image:width" content="512">
<meta property="og:image:height" content="512">
<meta property="og:image:alt" content="…">
<meta property="og:locale" content="fr_FR">
<meta property="og:site_name" content="ORGANISATION KA INTERNATIONALE">
```
### Twitter Cards
```html
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
<meta name="twitter:image" content="...">
```
`summary_large_image` + url, title, description, image, image:alt — self-hébergées comme tout le reste.
## 📊 Structured Data (JSON-LD)
### Organization Schema
Données structurées conformes à Schema.org pour améliorer l'affichage dans les résultats de recherche.
Deux blocs injectés par `Seo.svelte` sur toutes les pages :
**Type :** Organization
1. **Organization** : nom (OKI), URL, logo, description, adresse (36 Rue Lethière, Basse-Terre 97100, GP), contact (kontak@o-k-i.net), sameAs (niyaj, kute, labola, liberapay), `foundingDate: 2024`, `nonprofitStatus`.
2. **MusicGroup** : DJANGOKAM (genres Reggae/Zouk/Konpa/K-pop, djangokam.pawol.nu, membre OKI).
**Informations incluses :**
- Nom : ORGANISATION KA INTERNATIONALE
- Nom alternatif : OKI
- URL : https://o-k-i.net
- Logo
- Description
- Adresse physique (Basse-Terre, Guadeloupe)
- Email de contact
- Liens vers les plateformes sociales (GADE, BOKANTE, NIYAJ, KUTE, Discord, Labola, Liberapay)
- Statut : Organisation à but non lucratif
## 🔍 Attributs lang
## 🔍 Optimisations par page
### Page d'accueil
- **Title :** "ORGANISATION KA INTERNATIONALE | OKI"
- **Description :** Reprend site.description
- **Keywords :** Fédiverse, logiciel libre, alternatives GAFAM, Guadeloupe
### Mentions légales
- **Title :** "Mentions Légales | ORGANISATION KA INTERNATIONALE"
- **Description :** "Mentions légales et informations juridiques de l'Organisation Ka Internationale (OKI) - Hébergement, RGPD, licence AGPL-V3"
L'attribut `lang` du `<html>` est posé au prerender par `src/hooks.server.ts` (`fr` par défaut, `en` sous `/en/`), et `data-locale` pilote la redirection de langue (`static/lang-redirect.js`). Les microcopies en kréyòl (404, hors-ligne) portent `lang="gcf"`.
## 🎯 Bonnes pratiques
### Ajouter une nouvelle page
1. Créer le fichier `.njk` dans `src/`
2. Ajouter le front matter avec title et description personnalisés
3. Mettre à jour `src/sitemap.xml`
4. Ajouter un lien dans la navigation si nécessaire
### Personnaliser les meta tags
Dans le front matter de la page :
```yaml
---
layout: layouts/base.njk
title: Titre de la page
description: Description spécifique à cette page
---
```
1. Créer `src/routes/ma-page/+page.svelte` avec `<Seo>` renseigné
2. Ajouter la paire FR↔EN dans `PATH_PAIRS` (`src/lib/i18n/index.ts`)
3. Mettre à jour `static/sitemap.xml`
4. Ajouter un lien dans `src/lib/i18n/*/navigation.json` si nécessaire
## 🚀 Tests et validation
### Outils recommandés
1. **Google Search Console** - Soumettre le sitemap
2. **Facebook Sharing Debugger** - Tester les meta Open Graph
- URL : https://developers.facebook.com/tools/debug/
3. **Twitter Card Validator** - Tester les Twitter Cards
- URL : https://cards-dev.twitter.com/validator
4. **Schema Markup Validator** - Valider le JSON-LD
- URL : https://validator.schema.org/
5. **Google Rich Results Test** - Tester les données structurées
- URL : https://search.google.com/test/rich-results
2. **Facebook Sharing Debugger** - https://developers.facebook.com/tools/debug/
3. **Schema Markup Validator** - https://validator.schema.org/
4. **Google Rich Results Test** - https://search.google.com/test/rich-results
### Commandes utiles
### Commandes utiles (sur `npm run preview`)
```bash
# Vérifier robots.txt
curl http://localhost:8080/robots.txt
# Vérifier sitemap.xml
curl http://localhost:8080/sitemap.xml
# Vérifier les meta tags
curl http://localhost:8080/ | grep -E "(og:|twitter:)"
curl http://localhost:4173/robots.txt
curl http://localhost:4173/sitemap.xml
curl http://localhost:4173/ | grep -oE '(property|name)="(og:|twitter:)[^"]*"'
```
## 📈 Suivi et amélioration
### Métriques à surveiller
- Position dans les résultats de recherche Google
- Taux de clics (CTR) depuis les résultats de recherche
- Nombre de pages indexées
- Erreurs d'exploration
### Améliorations futures possibles
- [ ] Ajouter un blog/actualités avec articles optimisés SEO
- [ ] Créer des pages dédiées pour chaque service Fédiverse
- [ ] Ajouter des FAQ avec Schema.org FAQPage
- [ ] Optimiser les images avec attributs alt descriptifs
- [ ] Ajouter breadcrumbs avec données structurées
- [ ] Implémenter AMP (Accelerated Mobile Pages) si nécessaire
- [ ] Ajouter hreflang pour versions multilingues (créole, anglais, etc.)
- [ ] Pages dédiées pour chaque service Fédiverse
- [ ] FAQ avec Schema.org FAQPage
- [ ] Breadcrumbs avec données structurées
- [ ] Hreflang pour d'autres langues (créole…)
## 🌍 Mots-clés ciblés
### Primaires
- Organisation Ka Internationale
- OKI
- Fédiverse Guadeloupe
- PeerTube Guadeloupe
- Alternatives GAFAM
- Organisation Ka Internationale · OKI · Fédiverse Guadeloupe · Alternatives GAFAM
### Secondaires
- Pleroma
- Funkwhale
- Castopod
- Logiciel libre Antilles
- Médias libres Caraïbes
- Hébergement éthique
- Langues créoles
- Réseau social libre
- PeerTube / Mastodon / Nextcloud / Castopod Guadeloupe · Logiciel libre Antilles · Médias libres Caraïbes · Hébergement éthique · Langues créoles
### Longue traîne
- "alternative libre à YouTube Guadeloupe"
- "hébergement PeerTube Martinique"
- "réseau social décentralisé Antilles"
- "promotion langues créoles numérique"
- "alternative libre à YouTube Guadeloupe" · "hébergement PeerTube Martinique" · "réseau social décentralisé Antilles" · "promotion langues créoles numérique"
## 📝 Checklist SEO
- [x] Robots.txt configuré
- [x] Sitemap.xml créé et soumis
- [x] Sitemap.xml (8 URLs, dates à jour)
- [x] Meta description sur toutes les pages
- [x] Balises title optimisées
- [x] URL canoniques
- [x] Open Graph tags
- [x] Open Graph (10 balises, og:image self-hébergée)
- [x] Twitter Cards
- [x] JSON-LD Organization
- [x] Logo haute résolution (512x512)
- [ ] Images avec attributs alt
- [ ] Performance optimale (Lighthouse score)
- [x] Mobile-friendly (déjà responsive)
- [x] JSON-LD Organization + MusicGroup
- [x] Logo haute résolution (512×512)
- [x] Images avec attributs alt
- [x] Performance (Lighthouse mobile ≥ 90, mesuré à 91)
- [x] Mobile-friendly (responsive, vérifié Lighthouse)
- [x] HTTPS activé et HSTS configuré
- [x] Hreflang FR/EN pour les versions multilingues
- [x] Hreflang FR/EN/x-default
- [ ] Google Search Console configuré
- [ ] Analytics installé (optionnel, respectueux RGPD)
- [ ] Analytics installé (optionnel, respectueux RGPD — rester sans tiers)
+3 -2
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import { resolve } from '$app/paths';
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
import type { Bundle, Locale } from '$lib/i18n';
let { t, locale }: { t: Bundle; locale: Locale } = $props();
const year = new Date().getFullYear();
const legalHref = $derived(locale === 'en' ? '/en/legal-notice/' : '/mentions-legales/');
const legalPath = $derived(locale === 'en' ? '/en/legal-notice/' : '/mentions-legales/');
</script>
<footer>
@@ -14,7 +15,7 @@
<p class="footer-name">{t.site.name}</p>
<p class="footer-meta">© {year} {t.site.name} | {t.site.tagline}</p>
<p class="footer-legal">
<a href={legalHref}>{t.footer.legal_link}</a>
<a href={resolve(legalPath)}>{t.footer.legal_link}</a>
</p>
<div class="footer-socials">
<a href="mailto:kontak@o-k-i.net" title="Email" class="social-link" aria-label="Email">
+14 -9
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { resolve } from '$app/paths';
import { alternatePath, type Bundle, type Locale } from '$lib/i18n';
let { t, locale }: { t: Bundle; locale: Locale } = $props();
@@ -40,7 +41,7 @@
<nav aria-label={t.ui?.nav_label ?? 'Navigation principale'}>
<div class="nav-container">
<a href={locale === 'en' ? '/en/' : '/'} class="logo" onclick={closeMenu}>
<a href={resolve(locale === 'en' ? '/en/' : '/')} class="logo" onclick={closeMenu}>
<img src="/images/logo-512x512.png" alt="{t.site.shortName} Logo" class="logo-image" width="40" height="40" />
<span>{t.site.shortName}</span>
</a>
@@ -61,25 +62,29 @@
<ul class="nav-dropdown">
{#each item.children as child (child.text)}
<li>
<a
href={child.url}
target={child.external ? '_blank' : undefined}
rel={child.external ? 'noopener noreferrer' : undefined}
onclick={closeMenu}>{child.text}</a
>
{#if child.external}
<a
href={child.url}
target="_blank"
rel="external noopener noreferrer"
onclick={closeMenu}>{child.text}</a
>
{:else}
<a href={resolve(child.url)} onclick={closeMenu}>{child.text}</a>
{/if}
</li>
{/each}
</ul>
</li>
{:else}
<li>
<a href={item.url} onclick={closeMenu}>{item.text}</a>
<a href={resolve(item.url)} onclick={closeMenu}>{item.text}</a>
</li>
{/if}
{/each}
</ul>
<a href={altHref} class="lang-switch" hreflang={altLocale} onclick={rememberLangChoice}>
<a href={resolve(altHref)} class="lang-switch" hreflang={altLocale} onclick={rememberLangChoice}>
{t.ui.lang_switch_label}
</a>
+7 -2
View File
@@ -66,6 +66,11 @@
url: 'https://o-k-i.net'
}
});
// {@html} sur contenu first-party (JSON-LD généré au build, aucune donnée utilisateur).
// `<\/script>` : échappement requis pour ne pas fermer le bloc <script> du composant.
const orgJsonLdScript = $derived(`<script type="application/ld+json">${orgJsonLd}<\/script>`);
const djangokamJsonLdScript = `<script type="application/ld+json">${djangokamJsonLd}<\/script>`;
</script>
<svelte:head>
@@ -97,6 +102,6 @@
<meta name="twitter:image" content="https://o-k-i.net/images/logo-512x512.png" />
<meta name="twitter:image:alt" content="Logo {site.name}" />
{@html `<script type="application/ld+json">${orgJsonLd}</script>`}
{@html `<script type="application/ld+json">${djangokamJsonLd}</script>`}
{@html orgJsonLdScript}
{@html djangokamJsonLdScript}
</svelte:head>
+2 -2
View File
@@ -19,7 +19,7 @@
<section id="clients" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.clients.section_title} />{' '}
<KineticText as="span" text={t.clients.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.clients.section_title_accent} /></span
>
@@ -28,7 +28,7 @@
<div class="clients-grid">
{#each t.clients.items as client (client.domain)}
<a href={client.url} target="_blank" rel="noopener noreferrer" class="client-card">
<a href={client.url} target="_blank" rel="external noopener noreferrer" class="client-card">
<div class="client-logo-container">
<ResponsiveImage
picture={LOGOS[client.logo]}
@@ -1,4 +1,5 @@
<script lang="ts">
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
import KineticText from '$lib/components/motion/KineticText.svelte';
import type { Bundle, Locale } from '$lib/i18n';
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
import KineticText from '$lib/components/motion/KineticText.svelte';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import type { Bundle, Locale } from '$lib/i18n';
@@ -22,7 +23,7 @@
<div class="container">
<p class="djangokam-surtitre">{t.djangokam.surtitre}</p>
<h2 class="section-title">
<KineticText as="span" text={t.djangokam.titre} />{' '}
<KineticText as="span" text={t.djangokam.titre} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.djangokam.titreAccent} /></span
>
@@ -48,7 +49,7 @@
</div>
<figure class="djangokam-duo">
<a href={t.djangokam.duo.url} target="_blank" rel="noopener noreferrer">
<a href={t.djangokam.duo.url} target="_blank" rel="external noopener noreferrer">
<ResponsiveImage
picture={POCHETTES[t.djangokam.duo.image]}
alt={t.djangokam.duo.alt}
@@ -77,7 +78,7 @@
<div class="djangokam-clips">
{#each t.djangokam.clips as clip (clip.titre)}
<figure class="djangokam-clip">
<a href={clip.url} target="_blank" rel="noopener noreferrer">
<a href={clip.url} target="_blank" rel="external noopener noreferrer">
<ResponsiveImage
picture={POCHETTES[clip.image]}
alt={clip.alt}
@@ -97,7 +98,7 @@
<a
href={bouton.url}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
class="btn"
class:btn-solid={bouton.primary}
>
@@ -9,7 +9,7 @@
<section id="engagement" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.engagement.section_title} />{' '}
<KineticText as="span" text={t.engagement.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.engagement.section_title_accent} /></span
>
+1 -1
View File
@@ -8,7 +8,7 @@
<section id="faq" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.faq.section_title} />{' '}
<KineticText as="span" text={t.faq.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.faq.section_title_accent} /></span
>
+1 -1
View File
@@ -9,7 +9,7 @@
<section id="services-libres" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.services.section_title} />{' '}
<KineticText as="span" text={t.services.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.services.section_title_accent} /></span
>
+1 -1
View File
@@ -8,7 +8,7 @@
<section id="hebergement" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.hosting.section_title} />{' '}
<KineticText as="span" text={t.hosting.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.hosting.section_title_accent} /></span
>
@@ -104,7 +104,7 @@
class="bldg"
href={niyaj.url}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
aria-label="{niyaj.name}{niyaj.platform}"
>
<g class="bldg-art">
@@ -139,7 +139,7 @@
class="bldg"
href={gade.url}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
aria-label="{gade.name}{gade.platform}"
>
<g class="bldg-art">
@@ -172,7 +172,7 @@
class="bldg"
href={bokante.url}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
aria-label="{bokante.name}{bokante.platform}"
>
<g class="bldg-art">
@@ -209,7 +209,7 @@
class="bldg"
href={kute.url}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
aria-label="{kute.name}{kute.platform}"
>
<g class="bldg-art">
@@ -259,7 +259,7 @@
<span class="tag">{item.platform}</span>
</div>
<p class="vf-desc">{item.description}</p>
<a class="vf-link" href={item.url} target="_blank" rel="noopener noreferrer"
<a class="vf-link" href={item.url} target="_blank" rel="external noopener noreferrer"
>{item.linkText}</a
>
</li>
+1 -1
View File
@@ -9,7 +9,7 @@
<section id="mission" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.mission.section_title} />{' '}
<KineticText as="span" text={t.mission.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.mission.section_title_accent} /></span
>
+2 -2
View File
@@ -19,7 +19,7 @@
<section id="partenaires" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.partners.section_title} />{' '}
<KineticText as="span" text={t.partners.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.partners.section_title_accent} /></span
>
@@ -41,7 +41,7 @@
<p class="partner-description">{partner.description}</p>
<div class="partner-links">
{#each partner.links as link (link.url)}
<a href={link.url} target="_blank" rel="noopener noreferrer" class="partner-link">
<a href={link.url} target="_blank" rel="external noopener noreferrer" class="partner-link">
{link.text}
</a>
{/each}
+2 -2
View File
@@ -9,7 +9,7 @@
<section id="projets" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.projects.section_title} />{' '}
<KineticText as="span" text={t.projects.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.projects.section_title_accent} /></span
>
@@ -17,7 +17,7 @@
<div class="project-cards">
{#each t.projects.items as item (item.name)}
<a href={item.url} target="_blank" rel="noopener noreferrer" class="project-card">
<a href={item.url} target="_blank" rel="external noopener noreferrer" class="project-card">
<div class="project-icon">
<svg class="icon" aria-hidden="true"
><use href="/icons.svg#{pictoFor(item.icon)}" /></svg
+1 -1
View File
@@ -9,7 +9,7 @@
<section id="solutions" class="section">
<div class="container">
<h2 class="section-title">
<KineticText as="span" text={t.publics.section_title} />{' '}
<KineticText as="span" text={t.publics.section_title} />&#32;
<span class="accent-text"
><KineticText as="span" text={t.publics.section_title_accent} /></span
>
+4 -2
View File
@@ -4,6 +4,8 @@
* FR par défaut ; EN sur /en/.
*/
import type { Pathname } from '$app/types';
export type Locale = 'fr' | 'en';
/** Bundle de traductions : une entrée par fichier JSON de section. */
@@ -39,7 +41,7 @@ export function localeFromPath(pathname: string): Locale {
}
/** Correspondances des pages entre locales (URLs historiques préservées). */
const PATH_PAIRS: Array<[fr: string, en: string]> = [
const PATH_PAIRS: Array<[fr: Pathname, en: Pathname]> = [
['/', '/en/'],
['/dons/', '/en/donate/'],
['/presse/', '/en/press/'],
@@ -47,7 +49,7 @@ const PATH_PAIRS: Array<[fr: string, en: string]> = [
];
/** Chemin équivalent dans l'autre locale (sans le hash). */
export function alternatePath(pathname: string, target: Locale): string {
export function alternatePath(pathname: string, target: Locale): Pathname {
for (const [fr, en] of PATH_PAIRS) {
if (pathname === fr) return target === 'en' ? en : fr;
if (pathname === en) return target === 'fr' ? fr : en;
+2 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { resolve } from '$app/paths';
const status = $derived(page.status);
const is404 = $derived(status === 404);
@@ -19,7 +20,7 @@
{:else}
<p class="fr">{page.error?.message ?? 'Une erreur est survenue.'}</p>
{/if}
<a class="btn" href="/">Retour à l'accueil</a>
<a class="btn" href={resolve('/')}>Retour à l'accueil</a>
</section>
<style>
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
import Seo from '$lib/components/Seo.svelte';
import donations from '$lib/data/donations.json';
import { getBundle } from '$lib/i18n';
@@ -46,7 +47,7 @@
<div class="donation-grid">
{#each donations.oneTime as d (d.url)}
<a href={d.url} class="donation-amount" target="_blank" rel="noopener noreferrer">
<a href={d.url} class="donation-amount" target="_blank" rel="external noopener noreferrer">
{d.amount}{t.support.one_time_suffix}
</a>
{/each}
@@ -86,7 +87,7 @@
href={d.url}
class="donation-amount monthly"
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
>
{d.amount}{t.support.monthly_suffix}
</a>
@@ -126,7 +127,7 @@
<a
href={donations.liberapay}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
class="alt-donation-card alt-donation-card--liberapay"
>
<svg class="icon alt-donation-icon" aria-hidden="true"
@@ -139,7 +140,7 @@
<a
href={donations.kofi}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
class="alt-donation-card alt-donation-card--kofi"
>
<svg class="icon alt-donation-icon" aria-hidden="true"
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
// {@html} sur contenu first-party (JSON i18n du dépôt), aucune donnée utilisateur.
import Seo from '$lib/components/Seo.svelte';
import donations from '$lib/data/donations.json';
import { getBundle } from '$lib/i18n';
@@ -46,7 +47,7 @@
<div class="donation-grid">
{#each donations.oneTime as d (d.url)}
<a href={d.url} class="donation-amount" target="_blank" rel="noopener noreferrer">
<a href={d.url} class="donation-amount" target="_blank" rel="external noopener noreferrer">
{d.amount}{t.support.one_time_suffix}
</a>
{/each}
@@ -86,7 +87,7 @@
href={d.url}
class="donation-amount monthly"
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
>
{d.amount}{t.support.monthly_suffix}
</a>
@@ -126,7 +127,7 @@
<a
href={donations.liberapay}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
class="alt-donation-card alt-donation-card--liberapay"
>
<svg class="icon alt-donation-icon" aria-hidden="true"
@@ -139,7 +140,7 @@
<a
href={donations.kofi}
target="_blank"
rel="noopener noreferrer"
rel="external noopener noreferrer"
class="alt-donation-card alt-donation-card--kofi"
>
<svg class="icon alt-donation-icon" aria-hidden="true"
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import Seo from '$lib/components/Seo.svelte';
import { getBundle } from '$lib/i18n';
@@ -212,7 +213,7 @@
<!-- Back to Home -->
<div class="legal-back">
<a href="/en/" class="btn btn-solid">{t.ui.back_home}</a>
<a href={resolve('/en/')} class="btn btn-solid">{t.ui.back_home}</a>
</div>
</div>
</section>
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { asset } from '$app/paths';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import Seo from '$lib/components/Seo.svelte';
import KineticText from '$lib/components/motion/KineticText.svelte';
@@ -48,18 +49,18 @@
<div class="press-visual-grid">
{#each t.press.visuals as visual, i (visual.pdf)}
<div class="press-visual">
<a href={pictures[i].img.src} target="_blank" rel="noopener noreferrer">
<a href={asset(pictures[i].img.src)} target="_blank" rel="noopener noreferrer">
<ResponsiveImage picture={pictures[i]} alt={visual.caption} loading="lazy" />
<span class="press-visual-caption">{visual.caption}</span>
</a>
<a href="/assets/press/{visual.pdf}" class="press-visual-pdf-link">
<a href={asset(`/assets/press/${visual.pdf}`)} class="press-visual-pdf-link">
{t.press.visual_pdf_label}
</a>
</div>
{/each}
</div>
<div class="press-visuals-pdf">
<a href="/assets/press/{t.press.visuals_pdf_file}" class="btn btn-solid">
<a href={asset(`/assets/press/${t.press.visuals_pdf_file}`)} class="btn btn-solid">
{t.press.visuals_pdf_label}
</a>
</div>
@@ -71,7 +72,7 @@
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
<div class="press-links-list">
{#each t.press.links as link (link.url)}
<a href={link.url} target="_blank" rel="noopener noreferrer">{link.text}</a>
<a href={link.url} target="_blank" rel="external noopener noreferrer">{link.text}</a>
{/each}
</div>
</div>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import Seo from '$lib/components/Seo.svelte';
import { getBundle } from '$lib/i18n';
@@ -221,7 +222,7 @@
<!-- Retour à l'accueil -->
<div class="legal-back">
<a href="/" class="btn btn-solid">{t.ui.back_home}</a>
<a href={resolve('/')} class="btn btn-solid">{t.ui.back_home}</a>
</div>
</div>
</section>
+5 -1
View File
@@ -1,3 +1,7 @@
<script lang="ts">
import { resolve } from '$app/paths';
</script>
<svelte:head>
<title>Hors ligne · ORGANISATION KA INTERNATIONALE</title>
<meta name="robots" content="noindex" />
@@ -12,7 +16,7 @@
rétablie.
</p>
<p class="fr" lang="en">This page is not available offline. It will be back as soon as your connection is restored.</p>
<a class="btn" href="/">Retour à l'accueil</a>
<a class="btn" href={resolve('/')}>Retour à l'accueil</a>
</section>
<style>
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { asset } from '$app/paths';
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
import Seo from '$lib/components/Seo.svelte';
import KineticText from '$lib/components/motion/KineticText.svelte';
@@ -48,18 +49,18 @@
<div class="press-visual-grid">
{#each t.press.visuals as visual, i (visual.pdf)}
<div class="press-visual">
<a href={pictures[i].img.src} target="_blank" rel="noopener noreferrer">
<a href={asset(pictures[i].img.src)} target="_blank" rel="noopener noreferrer">
<ResponsiveImage picture={pictures[i]} alt={visual.caption} loading="lazy" />
<span class="press-visual-caption">{visual.caption}</span>
</a>
<a href="/assets/press/{visual.pdf}" class="press-visual-pdf-link">
<a href={asset(`/assets/press/${visual.pdf}`)} class="press-visual-pdf-link">
{t.press.visual_pdf_label}
</a>
</div>
{/each}
</div>
<div class="press-visuals-pdf">
<a href="/assets/press/{t.press.visuals_pdf_file}" class="btn btn-solid">
<a href={asset(`/assets/press/${t.press.visuals_pdf_file}`)} class="btn btn-solid">
{t.press.visuals_pdf_label}
</a>
</div>
@@ -71,7 +72,7 @@
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
<div class="press-links-list">
{#each t.press.links as link (link.url)}
<a href={link.url} target="_blank" rel="noopener noreferrer">{link.text}</a>
<a href={link.url} target="_blank" rel="external noopener noreferrer">{link.text}</a>
{/each}
</div>
</div>