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  
- Renommage svelte_core_bestpractices.md
- Passe svelte-autofixer : 0 issue restante hors {@html} first-party assumés
This commit is contained in:
+65
-100
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
Merci de votre intérêt pour contribuer au site de l'ORGANISATION KA INTERNATIONALE !
|
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
|
## 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é
|
### 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
|
```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
|
### Ajouter un projet web
|
||||||
|
|
||||||
Éditez `src/_data/i18n/fr/projects.json` (et la version `en/`) :
|
É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.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "NOM.PROJET",
|
|
||||||
"icon": "📱",
|
|
||||||
"domain": "nom.projet",
|
|
||||||
"description": "Description du projet",
|
|
||||||
"url": "https://nom.projet/"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ajouter un client ou partenaire
|
### 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
|
### Ajouter une valeur
|
||||||
|
|
||||||
Éditez `src/_data/i18n/fr/values.json` (et la version `en/`) :
|
Éditez `src/lib/i18n/fr/values.json` (et `en/`). Le champ `icon` suit le même mapping que les projets (`pictos.ts`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"icon": "🌟",
|
|
||||||
"title": "Titre",
|
|
||||||
"description": "Description de la valeur"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modifier la navigation
|
### Modifier la navigation
|
||||||
|
|
||||||
Éditez `src/_data/i18n/fr/navigation.json` (et la version `en/`) :
|
Éditez `src/lib/i18n/fr/navigation.json` (et `en/`) :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "text": "Texte du lien", "url": "/#ancre", "external": false }
|
||||||
"text": "Texte du lien",
|
|
||||||
"url": "#section",
|
|
||||||
"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
|
### 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`).
|
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`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "💬 Communauté",
|
|
||||||
"links": [
|
|
||||||
{ "text": "Discord", "url": "https://discord.gg/DPvUCA8dD" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modifier les styles
|
## Modifier les styles
|
||||||
|
|
||||||
Les styles sont dans `src/assets/css/styles.css`.
|
- **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`.
|
||||||
### Variables CSS principales
|
- **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`.
|
||||||
```css
|
- 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.
|
||||||
--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;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Créer une nouvelle page
|
## 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
|
```svelte
|
||||||
---
|
<script lang="ts">
|
||||||
layout: layouts/base.njk
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
title: Titre de la page
|
import { getBundle } from '$lib/i18n';
|
||||||
description: Description pour le SEO
|
|
||||||
---
|
|
||||||
|
|
||||||
<section>
|
const t = getBundle('fr');
|
||||||
<h1>{{ title }}</h1>
|
</script>
|
||||||
<p>Contenu de votre page</p>
|
|
||||||
|
<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>
|
</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/` :
|
```bash
|
||||||
|
npx @sveltejs/mcp svelte-autofixer ./src/lib/components/sections/MonComposant.svelte
|
||||||
- `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
|
|
||||||
|
|
||||||
## Workflow de développement
|
## Workflow de développement
|
||||||
|
|
||||||
@@ -146,12 +113,12 @@ Les partials sont dans `src/_includes/partials/` :
|
|||||||
|
|
||||||
3. Testez localement :
|
3. Testez localement :
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Buildez pour vérifier :
|
4. Vérifiez types, accessibilité et build :
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run check && npm run build && npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Commitez vos changements :
|
5. Commitez vos changements :
|
||||||
@@ -167,24 +134,22 @@ Les partials sont dans `src/_includes/partials/` :
|
|||||||
|
|
||||||
## Standards de code
|
## Standards de code
|
||||||
|
|
||||||
### HTML/Nunjucks
|
### Svelte / TypeScript
|
||||||
- Indentation : 4 espaces
|
- Svelte 5 runes uniquement (`$state`, `$derived`, `$props`…) — voir `svelte_core_bestpractices.md`
|
||||||
- Toujours fermer les balises
|
- TypeScript strict, props typées
|
||||||
- Attributs entre guillemets doubles
|
- `onclick={...}` (jamais `on:click`), blocs `{#each}` à clé unique
|
||||||
|
- `svelte-autofixer` propre sur tout composant modifié, `npm run check` à zéro warning
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
- Indentation : 4 espaces
|
- Indentation : tabulations (convention du projet)
|
||||||
- Utiliser les variables CSS quand possible
|
- Variables des tokens quand possible, mobile-first
|
||||||
- Mobile-first pour le responsive
|
- Angles nets : `border-radius` ≤ `var(--radius-md)` (6 px)
|
||||||
|
|
||||||
### JavaScript
|
### JavaScript (fichiers statiques)
|
||||||
- Indentation : 4 espaces
|
- Vanilla JS, commentaires en français, aucun script inline dans le HTML (CSP `script-src 'self'`)
|
||||||
- Vanilla JS (pas de framework)
|
|
||||||
- Commentaires en français
|
|
||||||
- Gestion des erreurs
|
|
||||||
|
|
||||||
### Sécurité
|
### 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.
|
- Conserver les headers de sécurité lors d'une modification du serveur.
|
||||||
|
|
||||||
## Questions ?
|
## Questions ?
|
||||||
|
|||||||
@@ -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**.
|
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
|
## Licence
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Guide SEO - o-k-i.net
|
# 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
|
## 📋 Fichiers SEO
|
||||||
|
|
||||||
### 1. robots.txt
|
### 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.
|
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
|
### 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 :**
|
- `/` et `/en/` (priorité 1.0, hebdomadaire)
|
||||||
- Page d'accueil (priorité 1.0, hebdomadaire)
|
- `/dons/` et `/en/donate/` (0.6, mensuelle)
|
||||||
- Mentions légales (priorité 0.3, 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
|
||||||
|
|
||||||
### Meta Tags de base
|
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 :
|
||||||
Définis dans `src/_includes/layouts/base.njk` :
|
|
||||||
|
|
||||||
- **Title** : Dynamique selon la page
|
```svelte
|
||||||
- **Description** : Personnalisable par page via front matter
|
<Seo title="Titre de la page" description="Description spécifique" path="/ma-page/" locale="fr" />
|
||||||
- **Keywords** : OKI, Fédiverse, PeerTube, Pleroma, Funkwhale, Castopod, Guadeloupe, logiciel libre, GAFAM
|
```
|
||||||
- **Author** : Organisation Ka Internationale
|
|
||||||
- **Canonical URL** : Évite le contenu dupliqué
|
|
||||||
|
|
||||||
### 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
|
```html
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://o-k-i.net">
|
<meta property="og:url" content="https://o-k-i.net/…">
|
||||||
<meta property="og:title" content="...">
|
<meta property="og:title" content="…">
|
||||||
<meta property="og:description" content="...">
|
<meta property="og:description" content="…">
|
||||||
<meta property="og:image" content="https://o-k-i.net/assets/images/logo-512x512.png">
|
<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:locale" content="fr_FR">
|
||||||
|
<meta property="og:site_name" content="ORGANISATION KA INTERNATIONALE">
|
||||||
```
|
```
|
||||||
|
|
||||||
### Twitter Cards
|
### Twitter Cards
|
||||||
```html
|
`summary_large_image` + url, title, description, image, image:alt — self-hébergées comme tout le reste.
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:title" content="...">
|
|
||||||
<meta name="twitter:description" content="...">
|
|
||||||
<meta name="twitter:image" content="...">
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Structured Data (JSON-LD)
|
## 📊 Structured Data (JSON-LD)
|
||||||
|
|
||||||
### Organization Schema
|
Deux blocs injectés par `Seo.svelte` sur toutes les pages :
|
||||||
Données structurées conformes à Schema.org pour améliorer l'affichage dans les résultats de recherche.
|
|
||||||
|
|
||||||
**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 :**
|
## 🔍 Attributs lang
|
||||||
- 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
|
|
||||||
|
|
||||||
## 🔍 Optimisations par page
|
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"`.
|
||||||
|
|
||||||
### 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"
|
|
||||||
|
|
||||||
## 🎯 Bonnes pratiques
|
## 🎯 Bonnes pratiques
|
||||||
|
|
||||||
### Ajouter une nouvelle page
|
### Ajouter une nouvelle page
|
||||||
1. Créer le fichier `.njk` dans `src/`
|
1. Créer `src/routes/ma-page/+page.svelte` avec `<Seo>` renseigné
|
||||||
2. Ajouter le front matter avec title et description personnalisés
|
2. Ajouter la paire FR↔EN dans `PATH_PAIRS` (`src/lib/i18n/index.ts`)
|
||||||
3. Mettre à jour `src/sitemap.xml`
|
3. Mettre à jour `static/sitemap.xml`
|
||||||
4. Ajouter un lien dans la navigation si nécessaire
|
4. Ajouter un lien dans `src/lib/i18n/*/navigation.json` 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
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Tests et validation
|
## 🚀 Tests et validation
|
||||||
|
|
||||||
### Outils recommandés
|
### Outils recommandés
|
||||||
1. **Google Search Console** - Soumettre le sitemap
|
1. **Google Search Console** - Soumettre le sitemap
|
||||||
2. **Facebook Sharing Debugger** - Tester les meta Open Graph
|
2. **Facebook Sharing Debugger** - https://developers.facebook.com/tools/debug/
|
||||||
- URL : https://developers.facebook.com/tools/debug/
|
3. **Schema Markup Validator** - https://validator.schema.org/
|
||||||
3. **Twitter Card Validator** - Tester les Twitter Cards
|
4. **Google Rich Results Test** - https://search.google.com/test/rich-results
|
||||||
- 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
|
|
||||||
|
|
||||||
### Commandes utiles
|
### Commandes utiles (sur `npm run preview`)
|
||||||
```bash
|
```bash
|
||||||
# Vérifier robots.txt
|
curl http://localhost:4173/robots.txt
|
||||||
curl http://localhost:8080/robots.txt
|
curl http://localhost:4173/sitemap.xml
|
||||||
|
curl http://localhost:4173/ | grep -oE '(property|name)="(og:|twitter:)[^"]*"'
|
||||||
# Vérifier sitemap.xml
|
|
||||||
curl http://localhost:8080/sitemap.xml
|
|
||||||
|
|
||||||
# Vérifier les meta tags
|
|
||||||
curl http://localhost:8080/ | grep -E "(og:|twitter:)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📈 Suivi et amélioration
|
## 📈 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
|
### Améliorations futures possibles
|
||||||
- [ ] Ajouter un blog/actualités avec articles optimisés SEO
|
- [ ] Pages dédiées pour chaque service Fédiverse
|
||||||
- [ ] Créer des pages dédiées pour chaque service Fédiverse
|
- [ ] FAQ avec Schema.org FAQPage
|
||||||
- [ ] Ajouter des FAQ avec Schema.org FAQPage
|
- [ ] Breadcrumbs avec données structurées
|
||||||
- [ ] Optimiser les images avec attributs alt descriptifs
|
- [ ] Hreflang pour d'autres langues (créole…)
|
||||||
- [ ] 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.)
|
|
||||||
|
|
||||||
## 🌍 Mots-clés ciblés
|
## 🌍 Mots-clés ciblés
|
||||||
|
|
||||||
### Primaires
|
### Primaires
|
||||||
- Organisation Ka Internationale
|
- Organisation Ka Internationale · OKI · Fédiverse Guadeloupe · Alternatives GAFAM
|
||||||
- OKI
|
|
||||||
- Fédiverse Guadeloupe
|
|
||||||
- PeerTube Guadeloupe
|
|
||||||
- Alternatives GAFAM
|
|
||||||
|
|
||||||
### Secondaires
|
### Secondaires
|
||||||
- Pleroma
|
- PeerTube / Mastodon / Nextcloud / Castopod Guadeloupe · Logiciel libre Antilles · Médias libres Caraïbes · Hébergement éthique · Langues créoles
|
||||||
- Funkwhale
|
|
||||||
- Castopod
|
|
||||||
- Logiciel libre Antilles
|
|
||||||
- Médias libres Caraïbes
|
|
||||||
- Hébergement éthique
|
|
||||||
- Langues créoles
|
|
||||||
- Réseau social libre
|
|
||||||
|
|
||||||
### Longue traîne
|
### Longue traîne
|
||||||
- "alternative libre à YouTube Guadeloupe"
|
- "alternative libre à YouTube Guadeloupe" · "hébergement PeerTube Martinique" · "réseau social décentralisé Antilles" · "promotion langues créoles numérique"
|
||||||
- "hébergement PeerTube Martinique"
|
|
||||||
- "réseau social décentralisé Antilles"
|
|
||||||
- "promotion langues créoles numérique"
|
|
||||||
|
|
||||||
## 📝 Checklist SEO
|
## 📝 Checklist SEO
|
||||||
|
|
||||||
- [x] Robots.txt configuré
|
- [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] Meta description sur toutes les pages
|
||||||
- [x] Balises title optimisées
|
- [x] Balises title optimisées
|
||||||
- [x] URL canoniques
|
- [x] URL canoniques
|
||||||
- [x] Open Graph tags
|
- [x] Open Graph (10 balises, og:image self-hébergée)
|
||||||
- [x] Twitter Cards
|
- [x] Twitter Cards
|
||||||
- [x] JSON-LD Organization
|
- [x] JSON-LD Organization + MusicGroup
|
||||||
- [x] Logo haute résolution (512x512)
|
- [x] Logo haute résolution (512×512)
|
||||||
- [ ] Images avec attributs alt
|
- [x] Images avec attributs alt
|
||||||
- [ ] Performance optimale (Lighthouse score)
|
- [x] Performance (Lighthouse mobile ≥ 90, mesuré à 91)
|
||||||
- [x] Mobile-friendly (déjà responsive)
|
- [x] Mobile-friendly (responsive, vérifié Lighthouse)
|
||||||
- [x] HTTPS activé et HSTS configuré
|
- [x] HTTPS activé et HSTS configuré
|
||||||
- [x] Hreflang FR/EN pour les versions multilingues
|
- [x] Hreflang FR/EN/x-default
|
||||||
- [ ] Google Search Console configuré
|
- [ ] Google Search Console configuré
|
||||||
- [ ] Analytics installé (optionnel, respectueux RGPD)
|
- [ ] Analytics installé (optionnel, respectueux RGPD — rester sans tiers)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
|
import BrandIcon from '$lib/components/icons/BrandIcon.svelte';
|
||||||
import type { Bundle, Locale } from '$lib/i18n';
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
|
||||||
let { t, locale }: { t: Bundle; locale: Locale } = $props();
|
let { t, locale }: { t: Bundle; locale: Locale } = $props();
|
||||||
|
|
||||||
const year = new Date().getFullYear();
|
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>
|
</script>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<p class="footer-name">{t.site.name}</p>
|
<p class="footer-name">{t.site.name}</p>
|
||||||
<p class="footer-meta">© {year} {t.site.name} | {t.site.tagline}</p>
|
<p class="footer-meta">© {year} {t.site.name} | {t.site.tagline}</p>
|
||||||
<p class="footer-legal">
|
<p class="footer-legal">
|
||||||
<a href={legalHref}>{t.footer.legal_link}</a>
|
<a href={resolve(legalPath)}>{t.footer.legal_link}</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="footer-socials">
|
<div class="footer-socials">
|
||||||
<a href="mailto:kontak@o-k-i.net" title="Email" class="social-link" aria-label="Email">
|
<a href="mailto:kontak@o-k-i.net" title="Email" class="social-link" aria-label="Email">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import { alternatePath, type Bundle, type Locale } from '$lib/i18n';
|
import { alternatePath, type Bundle, type Locale } from '$lib/i18n';
|
||||||
|
|
||||||
let { t, locale }: { t: Bundle; locale: Locale } = $props();
|
let { t, locale }: { t: Bundle; locale: Locale } = $props();
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
|
|
||||||
<nav aria-label={t.ui?.nav_label ?? 'Navigation principale'}>
|
<nav aria-label={t.ui?.nav_label ?? 'Navigation principale'}>
|
||||||
<div class="nav-container">
|
<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" />
|
<img src="/images/logo-512x512.png" alt="{t.site.shortName} Logo" class="logo-image" width="40" height="40" />
|
||||||
<span>{t.site.shortName}</span>
|
<span>{t.site.shortName}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -61,25 +62,29 @@
|
|||||||
<ul class="nav-dropdown">
|
<ul class="nav-dropdown">
|
||||||
{#each item.children as child (child.text)}
|
{#each item.children as child (child.text)}
|
||||||
<li>
|
<li>
|
||||||
<a
|
{#if child.external}
|
||||||
href={child.url}
|
<a
|
||||||
target={child.external ? '_blank' : undefined}
|
href={child.url}
|
||||||
rel={child.external ? 'noopener noreferrer' : undefined}
|
target="_blank"
|
||||||
onclick={closeMenu}>{child.text}</a
|
rel="external noopener noreferrer"
|
||||||
>
|
onclick={closeMenu}>{child.text}</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<a href={resolve(child.url)} onclick={closeMenu}>{child.text}</a>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<a href={item.url} onclick={closeMenu}>{item.text}</a>
|
<a href={resolve(item.url)} onclick={closeMenu}>{item.text}</a>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</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}
|
{t.ui.lang_switch_label}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,11 @@
|
|||||||
url: 'https://o-k-i.net'
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -97,6 +102,6 @@
|
|||||||
<meta name="twitter:image" content="https://o-k-i.net/images/logo-512x512.png" />
|
<meta name="twitter:image" content="https://o-k-i.net/images/logo-512x512.png" />
|
||||||
<meta name="twitter:image:alt" content="Logo {site.name}" />
|
<meta name="twitter:image:alt" content="Logo {site.name}" />
|
||||||
|
|
||||||
{@html `<script type="application/ld+json">${orgJsonLd}</script>`}
|
{@html orgJsonLdScript}
|
||||||
{@html `<script type="application/ld+json">${djangokamJsonLd}</script>`}
|
{@html djangokamJsonLdScript}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<section id="clients" class="section">
|
<section id="clients" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.clients.section_title} />{' '}
|
<KineticText as="span" text={t.clients.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.clients.section_title_accent} /></span
|
><KineticText as="span" text={t.clients.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<div class="clients-grid">
|
<div class="clients-grid">
|
||||||
{#each t.clients.items as client (client.domain)}
|
{#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">
|
<div class="client-logo-container">
|
||||||
<ResponsiveImage
|
<ResponsiveImage
|
||||||
picture={LOGOS[client.logo]}
|
picture={LOGOS[client.logo]}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 BrandIcon from '$lib/components/icons/BrandIcon.svelte';
|
||||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
import type { Bundle, Locale } from '$lib/i18n';
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
import type { Bundle, Locale } from '$lib/i18n';
|
import type { Bundle, Locale } from '$lib/i18n';
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p class="djangokam-surtitre">{t.djangokam.surtitre}</p>
|
<p class="djangokam-surtitre">{t.djangokam.surtitre}</p>
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.djangokam.titre} />{' '}
|
<KineticText as="span" text={t.djangokam.titre} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.djangokam.titreAccent} /></span
|
><KineticText as="span" text={t.djangokam.titreAccent} /></span
|
||||||
>
|
>
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure class="djangokam-duo">
|
<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
|
<ResponsiveImage
|
||||||
picture={POCHETTES[t.djangokam.duo.image]}
|
picture={POCHETTES[t.djangokam.duo.image]}
|
||||||
alt={t.djangokam.duo.alt}
|
alt={t.djangokam.duo.alt}
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
<div class="djangokam-clips">
|
<div class="djangokam-clips">
|
||||||
{#each t.djangokam.clips as clip (clip.titre)}
|
{#each t.djangokam.clips as clip (clip.titre)}
|
||||||
<figure class="djangokam-clip">
|
<figure class="djangokam-clip">
|
||||||
<a href={clip.url} target="_blank" rel="noopener noreferrer">
|
<a href={clip.url} target="_blank" rel="external noopener noreferrer">
|
||||||
<ResponsiveImage
|
<ResponsiveImage
|
||||||
picture={POCHETTES[clip.image]}
|
picture={POCHETTES[clip.image]}
|
||||||
alt={clip.alt}
|
alt={clip.alt}
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
<a
|
<a
|
||||||
href={bouton.url}
|
href={bouton.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
class="btn"
|
class="btn"
|
||||||
class:btn-solid={bouton.primary}
|
class:btn-solid={bouton.primary}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section id="engagement" class="section">
|
<section id="engagement" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.engagement.section_title} />{' '}
|
<KineticText as="span" text={t.engagement.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.engagement.section_title_accent} /></span
|
><KineticText as="span" text={t.engagement.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<section id="faq" class="section">
|
<section id="faq" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.faq.section_title} />{' '}
|
<KineticText as="span" text={t.faq.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.faq.section_title_accent} /></span
|
><KineticText as="span" text={t.faq.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section id="services-libres" class="section">
|
<section id="services-libres" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.services.section_title} />{' '}
|
<KineticText as="span" text={t.services.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.services.section_title_accent} /></span
|
><KineticText as="span" text={t.services.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<section id="hebergement" class="section">
|
<section id="hebergement" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.hosting.section_title} />{' '}
|
<KineticText as="span" text={t.hosting.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.hosting.section_title_accent} /></span
|
><KineticText as="span" text={t.hosting.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
class="bldg"
|
class="bldg"
|
||||||
href={niyaj.url}
|
href={niyaj.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
aria-label="{niyaj.name} — {niyaj.platform}"
|
aria-label="{niyaj.name} — {niyaj.platform}"
|
||||||
>
|
>
|
||||||
<g class="bldg-art">
|
<g class="bldg-art">
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
class="bldg"
|
class="bldg"
|
||||||
href={gade.url}
|
href={gade.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
aria-label="{gade.name} — {gade.platform}"
|
aria-label="{gade.name} — {gade.platform}"
|
||||||
>
|
>
|
||||||
<g class="bldg-art">
|
<g class="bldg-art">
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
class="bldg"
|
class="bldg"
|
||||||
href={bokante.url}
|
href={bokante.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
aria-label="{bokante.name} — {bokante.platform}"
|
aria-label="{bokante.name} — {bokante.platform}"
|
||||||
>
|
>
|
||||||
<g class="bldg-art">
|
<g class="bldg-art">
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
class="bldg"
|
class="bldg"
|
||||||
href={kute.url}
|
href={kute.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
aria-label="{kute.name} — {kute.platform}"
|
aria-label="{kute.name} — {kute.platform}"
|
||||||
>
|
>
|
||||||
<g class="bldg-art">
|
<g class="bldg-art">
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
<span class="tag">{item.platform}</span>
|
<span class="tag">{item.platform}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="vf-desc">{item.description}</p>
|
<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
|
>{item.linkText}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section id="mission" class="section">
|
<section id="mission" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.mission.section_title} />{' '}
|
<KineticText as="span" text={t.mission.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.mission.section_title_accent} /></span
|
><KineticText as="span" text={t.mission.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<section id="partenaires" class="section">
|
<section id="partenaires" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.partners.section_title} />{' '}
|
<KineticText as="span" text={t.partners.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.partners.section_title_accent} /></span
|
><KineticText as="span" text={t.partners.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<p class="partner-description">{partner.description}</p>
|
<p class="partner-description">{partner.description}</p>
|
||||||
<div class="partner-links">
|
<div class="partner-links">
|
||||||
{#each partner.links as link (link.url)}
|
{#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} →
|
{link.text} →
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section id="projets" class="section">
|
<section id="projets" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.projects.section_title} />{' '}
|
<KineticText as="span" text={t.projects.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.projects.section_title_accent} /></span
|
><KineticText as="span" text={t.projects.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="project-cards">
|
<div class="project-cards">
|
||||||
{#each t.projects.items as item (item.name)}
|
{#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">
|
<div class="project-icon">
|
||||||
<svg class="icon" aria-hidden="true"
|
<svg class="icon" aria-hidden="true"
|
||||||
><use href="/icons.svg#{pictoFor(item.icon)}" /></svg
|
><use href="/icons.svg#{pictoFor(item.icon)}" /></svg
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section id="solutions" class="section">
|
<section id="solutions" class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<KineticText as="span" text={t.publics.section_title} />{' '}
|
<KineticText as="span" text={t.publics.section_title} /> 
|
||||||
<span class="accent-text"
|
<span class="accent-text"
|
||||||
><KineticText as="span" text={t.publics.section_title_accent} /></span
|
><KineticText as="span" text={t.publics.section_title_accent} /></span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* FR par défaut ; EN sur /en/.
|
* FR par défaut ; EN sur /en/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Pathname } from '$app/types';
|
||||||
|
|
||||||
export type Locale = 'fr' | 'en';
|
export type Locale = 'fr' | 'en';
|
||||||
|
|
||||||
/** Bundle de traductions : une entrée par fichier JSON de section. */
|
/** 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). */
|
/** 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/'],
|
['/', '/en/'],
|
||||||
['/dons/', '/en/donate/'],
|
['/dons/', '/en/donate/'],
|
||||||
['/presse/', '/en/press/'],
|
['/presse/', '/en/press/'],
|
||||||
@@ -47,7 +49,7 @@ const PATH_PAIRS: Array<[fr: string, en: string]> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/** Chemin équivalent dans l'autre locale (sans le hash). */
|
/** 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) {
|
for (const [fr, en] of PATH_PAIRS) {
|
||||||
if (pathname === fr) return target === 'en' ? en : fr;
|
if (pathname === fr) return target === 'en' ? en : fr;
|
||||||
if (pathname === en) return target === 'fr' ? fr : en;
|
if (pathname === en) return target === 'fr' ? fr : en;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
const status = $derived(page.status);
|
const status = $derived(page.status);
|
||||||
const is404 = $derived(status === 404);
|
const is404 = $derived(status === 404);
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<p class="fr">{page.error?.message ?? 'Une erreur est survenue.'}</p>
|
<p class="fr">{page.error?.message ?? 'Une erreur est survenue.'}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<a class="btn" href="/">Retour à l'accueil</a>
|
<a class="btn" href={resolve('/')}>Retour à l'accueil</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Seo from '$lib/components/Seo.svelte';
|
||||||
import donations from '$lib/data/donations.json';
|
import donations from '$lib/data/donations.json';
|
||||||
import { getBundle } from '$lib/i18n';
|
import { getBundle } from '$lib/i18n';
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
|
|
||||||
<div class="donation-grid">
|
<div class="donation-grid">
|
||||||
{#each donations.oneTime as d (d.url)}
|
{#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}
|
{d.amount}{t.support.one_time_suffix}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
href={d.url}
|
href={d.url}
|
||||||
class="donation-amount monthly"
|
class="donation-amount monthly"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
>
|
>
|
||||||
{d.amount}{t.support.monthly_suffix}
|
{d.amount}{t.support.monthly_suffix}
|
||||||
</a>
|
</a>
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
<a
|
<a
|
||||||
href={donations.liberapay}
|
href={donations.liberapay}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
class="alt-donation-card alt-donation-card--liberapay"
|
class="alt-donation-card alt-donation-card--liberapay"
|
||||||
>
|
>
|
||||||
<svg class="icon alt-donation-icon" aria-hidden="true"
|
<svg class="icon alt-donation-icon" aria-hidden="true"
|
||||||
@@ -139,7 +140,7 @@
|
|||||||
<a
|
<a
|
||||||
href={donations.kofi}
|
href={donations.kofi}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
class="alt-donation-card alt-donation-card--kofi"
|
class="alt-donation-card alt-donation-card--kofi"
|
||||||
>
|
>
|
||||||
<svg class="icon alt-donation-icon" aria-hidden="true"
|
<svg class="icon alt-donation-icon" aria-hidden="true"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 Seo from '$lib/components/Seo.svelte';
|
||||||
import donations from '$lib/data/donations.json';
|
import donations from '$lib/data/donations.json';
|
||||||
import { getBundle } from '$lib/i18n';
|
import { getBundle } from '$lib/i18n';
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
|
|
||||||
<div class="donation-grid">
|
<div class="donation-grid">
|
||||||
{#each donations.oneTime as d (d.url)}
|
{#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}
|
{d.amount}{t.support.one_time_suffix}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
href={d.url}
|
href={d.url}
|
||||||
class="donation-amount monthly"
|
class="donation-amount monthly"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
>
|
>
|
||||||
{d.amount}{t.support.monthly_suffix}
|
{d.amount}{t.support.monthly_suffix}
|
||||||
</a>
|
</a>
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
<a
|
<a
|
||||||
href={donations.liberapay}
|
href={donations.liberapay}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
class="alt-donation-card alt-donation-card--liberapay"
|
class="alt-donation-card alt-donation-card--liberapay"
|
||||||
>
|
>
|
||||||
<svg class="icon alt-donation-icon" aria-hidden="true"
|
<svg class="icon alt-donation-icon" aria-hidden="true"
|
||||||
@@ -139,7 +140,7 @@
|
|||||||
<a
|
<a
|
||||||
href={donations.kofi}
|
href={donations.kofi}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
class="alt-donation-card alt-donation-card--kofi"
|
class="alt-donation-card alt-donation-card--kofi"
|
||||||
>
|
>
|
||||||
<svg class="icon alt-donation-icon" aria-hidden="true"
|
<svg class="icon alt-donation-icon" aria-hidden="true"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
import { getBundle } from '$lib/i18n';
|
import { getBundle } from '$lib/i18n';
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@
|
|||||||
|
|
||||||
<!-- Back to Home -->
|
<!-- Back to Home -->
|
||||||
<div class="legal-back">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { asset } from '$app/paths';
|
||||||
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
@@ -48,18 +49,18 @@
|
|||||||
<div class="press-visual-grid">
|
<div class="press-visual-grid">
|
||||||
{#each t.press.visuals as visual, i (visual.pdf)}
|
{#each t.press.visuals as visual, i (visual.pdf)}
|
||||||
<div class="press-visual">
|
<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" />
|
<ResponsiveImage picture={pictures[i]} alt={visual.caption} loading="lazy" />
|
||||||
<span class="press-visual-caption">{visual.caption}</span>
|
<span class="press-visual-caption">{visual.caption}</span>
|
||||||
</a>
|
</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}
|
{t.press.visual_pdf_label}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="press-visuals-pdf">
|
<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} →
|
{t.press.visuals_pdf_label} →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
|
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
|
||||||
<div class="press-links-list">
|
<div class="press-links-list">
|
||||||
{#each t.press.links as link (link.url)}
|
{#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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
import { getBundle } from '$lib/i18n';
|
import { getBundle } from '$lib/i18n';
|
||||||
|
|
||||||
@@ -221,7 +222,7 @@
|
|||||||
|
|
||||||
<!-- Retour à l'accueil -->
|
<!-- Retour à l'accueil -->
|
||||||
<div class="legal-back">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Hors ligne · ORGANISATION KA INTERNATIONALE</title>
|
<title>Hors ligne · ORGANISATION KA INTERNATIONALE</title>
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
rétablie.
|
rétablie.
|
||||||
</p>
|
</p>
|
||||||
<p class="fr" lang="en">This page is not available offline. It will be back as soon as your connection is restored.</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>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { asset } from '$app/paths';
|
||||||
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
import ResponsiveImage from '$lib/components/ResponsiveImage.svelte';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Seo from '$lib/components/Seo.svelte';
|
||||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
@@ -48,18 +49,18 @@
|
|||||||
<div class="press-visual-grid">
|
<div class="press-visual-grid">
|
||||||
{#each t.press.visuals as visual, i (visual.pdf)}
|
{#each t.press.visuals as visual, i (visual.pdf)}
|
||||||
<div class="press-visual">
|
<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" />
|
<ResponsiveImage picture={pictures[i]} alt={visual.caption} loading="lazy" />
|
||||||
<span class="press-visual-caption">{visual.caption}</span>
|
<span class="press-visual-caption">{visual.caption}</span>
|
||||||
</a>
|
</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}
|
{t.press.visual_pdf_label}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="press-visuals-pdf">
|
<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} →
|
{t.press.visuals_pdf_label} →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
|
<KineticText text={t.press.links_title} as="h2" class="press-section-title" />
|
||||||
<div class="press-links-list">
|
<div class="press-links-list">
|
||||||
{#each t.press.links as link (link.url)}
|
{#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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user