Dimdam's
Dimdam’s Git

dimdams / dimdams-docs

Documentation de Dimdams

Public
00

feat: initialisation du site de documentation officiel Dimdam's Docs

Benladamm · il y a 9 hf61a416e48e9 Parcourir les fichiers à ce commit

63 fichiers modifiés · +4795 −0

Ajouté.env.example+10−0
@@ -0,0 +1,10 @@
1+# Dimdam's Docs. Every value is required: there is no default in the code. Read at build time
2+# (the site is static: rebuild after a change).
3+
4+# Public address of the documentation (canonical links, sitemap)
5+PUBLIC_URL="https://docs.dimdams.com"
6+# Main site (header button) and the panel (API base address, links to the dashboard)
7+MAIN_SITE_URL="https://www.dimdams.com"
8+# Dimdam's Git and Dimdam's Status (links and examples)
9+GIT_URL="https://git.dimdams.com"
10+STATUS_URL="https://status.dimdams.com"
Ajouté.gitattributes+3−0
@@ -0,0 +1,3 @@
1+* text=auto eol=lf
2+*.png binary
3+*.webp binary
Ajouté.gitignore+11−0
@@ -0,0 +1,11 @@
1+node_modules/
2+.next/
3+out/
4+*.log
5+*.tsbuildinfo
6+next-env.d.ts
7+
8+# Settings: never in the repository.
9+.env
10+.env.*
11+!.env.example
AjoutéREADME.md+32−0
@@ -0,0 +1,32 @@
1+# Dimdam's Docs
2+
3+Site de documentation officiel de Dimdam's ([docs.dimdams.com](https://docs.dimdams.com)), remplaçant l'ancien GitBook.
4+
5+Il regroupe toute la documentation des produits Dimdam's en 4 sections :
6+- **Hébergement** (`content/hebergement/`) : Commander, configurer et gérer sites web, bots, serveurs de jeu, VPS et bases de données.
7+- **API & CLI** (`content/api/`) : Piloter les services via l'API REST et le CLI `dimdams`.
8+- **Dimdam's Git** (`content/git/`) : Hébergement Git en France (code, CI/CD, paquets, Pages).
9+- **Dimdam's Status** (`content/status/`) : Suivi de la disponibilité et API de statut.
10+
11+## Architecture technique
12+
13+- **Framework :** Next.js 16 (App Router) avec export 100% statique (`output: "export"`, `trailingSlash: true`).
14+- **Markdown :** Moteur Shiki pour la coloration syntaxique et Marked pour le rendu HTML.
15+- **Recherche :** Index de recherche généré statiquement à la compilation (`/search-index.json`).
16+- **Hébergement :** Fichiers statiques (`out/`) servis directement par Nginx sur `server-003` et exposés via le tunnel Cloudflare.
17+
18+## Commandes
19+
20+```bash
21+# Développement local (port 3010)
22+npm run dev
23+
24+# Validation des fichiers Markdown et des liens internes
25+npm run check
26+
27+# Compilation statique (génère le dossier out/)
28+npm run build
29+
30+# Déploiement sur server-003
31+npm run deploy
32+```
Ajoutécontent/api/alertes.md+133−0
@@ -0,0 +1,133 @@
1+---
2+title: Alertes
3+description: Règles d'alerte (Discord, e-mail…) sur la consommation d'un service.
4+order: 10
5+---
6+
7+## Consulter les règles d'alerte
8+
9+`GET /api/hosts/{id}/alerts` · permission `hosts:read`
10+
11+Liste les seuils configurés sur le CPU, la RAM, l'espace disque ou l'arrêt inopiné de l'instance.
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant du service |
18+
19+```bash
20+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/alerts" \
21+ -H "Authorization: Bearer dim_live_…"
22+```
23+
24+Réponse :
25+
26+```json
27+{
28+ "success": true,
29+ "rules": [
30+ {
31+ "id": "cly_alert_001",
32+ "metricType": "CPU",
33+ "thresholdValue": 90.0,
34+ "durationSec": 600,
35+ "channelType": "DISCORD_WEBHOOK",
36+ "channelTarget": "https://discord.com/api/webhooks/...",
37+ "enabled": true
38+ }
39+ ]
40+}
41+```
42+
43+## Créer une règle d'alerte
44+
45+`POST /api/hosts/{id}/alerts` · permission `hosts:write`
46+
47+Configure une notification proactive envoyée sur Discord, par Email ou dans le tableau de bord en cas de dépassement critique de ressource.
48+
49+**Paramètres d'adresse**
50+
51+| Nom | Type | Requis | Description |
52+| --- | --- | --- | --- |
53+| `id` | string | oui | Identifiant du service |
54+
55+**Corps JSON**
56+
57+| Nom | Type | Requis | Description |
58+| --- | --- | --- | --- |
59+| `metricType` | string | oui | "CPU" \| "RAM" \| "DISK" \| "STATUS" |
60+| `thresholdValue` | number | oui | Pourcentage déclencheur (ex: 85.0) |
61+| `durationSec` | number | non | Durée continue avant alerte en secondes (défaut: 600) |
62+| `channelType` | string | oui | "DISCORD_WEBHOOK" \| "EMAIL" \| "IN_APP" |
63+| `channelTarget` | string | oui | URL du webhook Discord ou adresse email |
64+
65+```bash
66+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/alerts" \
67+ -H "Authorization: Bearer dim_live_…" \
68+ -H "Content-Type: application/json" \
69+ -d '{
70+ "metricType": "CPU",
71+ "thresholdValue": 90,
72+ "durationSec": 600,
73+ "channelType": "DISCORD_WEBHOOK",
74+ "channelTarget": "https://discord.com/api/webhooks/123/abc"
75+ }'
76+```
77+
78+Réponse :
79+
80+```json
81+{
82+ "success": true,
83+ "rule": {
84+ "id": "cly_alert_002",
85+ "metricType": "CPU",
86+ "thresholdValue": 90,
87+ "channelType": "DISCORD_WEBHOOK",
88+ "enabled": true
89+ }
90+}
91+```
92+
93+## Modifier une règle d'alerte
94+
95+`PUT /api/hosts/{id}/alerts/{ruleId}` · permission `hosts:write`
96+
97+Seuls les champs envoyés changent : `metricType`, `thresholdValue`, `durationSec`, `cooldownMin` (délai en minutes entre deux alertes), `enabled`, `channelType`, `channelTarget`.
98+
99+```bash
100+curl -X PUT "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/alerts/RULE_ID" \
101+ -H "Authorization: Bearer dim_live_…" \
102+ -H "Content-Type: application/json" \
103+ -d '{"thresholdValue": 90, "enabled": true}'
104+```
105+
106+## Supprimer une règle d'alerte
107+
108+`DELETE /api/hosts/{id}/alerts/{ruleId}` · permission `hosts:write`
109+
110+```bash
111+curl -X DELETE "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/alerts/RULE_ID" \
112+ -H "Authorization: Bearer dim_live_…"
113+```
114+
115+## Tester un canal de notification
116+
117+`POST /api/hosts/{id}/alerts/test` · permission `hosts:write`
118+
119+Envoie un message de test, par exemple sur un webhook Discord, pour vérifier le canal avant de créer la règle.
120+
121+**Corps JSON**
122+
123+| Nom | Type | Requis | Description |
124+| --- | --- | --- | --- |
125+| `channelType` | string | oui | `"DISCORD_WEBHOOK"`, `"EMAIL"` ou `"IN_APP"` |
126+| `channelTarget` | string | oui | URL du webhook Discord (`https://discord.com/api/webhooks/…`) ou adresse e-mail |
127+
128+```bash
129+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/alerts/test" \
130+ -H "Authorization: Bearer dim_live_…" \
131+ -H "Content-Type: application/json" \
132+ -d '{"channelType": "DISCORD_WEBHOOK", "channelTarget": "https://discord.com/api/webhooks/123/abc"}'
133+```
Ajoutécontent/api/cli.md+76−0
@@ -0,0 +1,76 @@
1+---
2+title: CLI dimdams
3+description: Gérer vos services depuis un terminal, avec la même clé que l'API.
4+order: 1
5+---
6+
7+La CLI `dimdams` est un seul fichier, sans dépendance. Elle utilise l'[API REST](/api/) et une [clé d'API](/api/#authentification).
8+
9+## Installation
10+
11+Il faut Node.js 18 ou plus récent.
12+
13+**Linux et macOS :**
14+
15+```bash
16+curl -fsSL {{MAIN_SITE_URL}}/cli/install.sh | sh
17+```
18+
19+**Windows :** téléchargez [`dimdams.mjs`]({{MAIN_SITE_URL}}/cli/dimdams.mjs), puis lancez-le avec Node :
20+
21+```bash
22+node dimdams.mjs --help
23+```
24+
25+## Se connecter
26+
27+```bash
28+dimdams login
29+```
30+
31+La commande demande votre clé d'API et l'enregistre dans votre dossier de configuration (`~/.config/dimdams/config.json`, ou `%APPDATA%\dimdams\config.json` sous Windows). `dimdams whoami` affiche le compte et les permissions de la clé, `dimdams logout` la supprime.
32+
33+Dans une CI, passez plutôt la clé par une variable d'environnement, depuis les secrets du pipeline :
34+
35+```bash
36+DIMDAMS_API_KEY="$DIMDAMS_API_KEY" dimdams restart mon-site
37+```
38+
39+## Commandes
40+
41+Un `<service>` se désigne par son identifiant, son nom ou son domaine.
42+
43+| Commande | Ce qu'elle fait | Permission |
44+| --- | --- | --- |
45+| `dimdams services` | Liste vos services | `hosts:read` |
46+| `dimdams status <service>` | Détails d'un service | `hosts:read` |
47+| `dimdams start <service>` | Démarre un service | `hosts:power` |
48+| `dimdams stop <service>` | Arrête un service | `hosts:power` |
49+| `dimdams restart <service>` | Redémarre un service | `hosts:power` |
50+| `dimdams logs <service>` | Affiche les journaux (`-n 200` pour le nombre de lignes, `--type docker\|access\|error`, `-f` pour suivre) | `hosts:read` |
51+| `dimdams deploy <service>` | Lance un déploiement Git | `hosts:write` |
52+| `dimdams backups <service>` | Liste les sauvegardes | `backups:read` |
53+| `dimdams backup <service>` | Crée une sauvegarde | `backups:write` |
54+
55+Exemples :
56+
57+```bash
58+dimdams services
59+dimdams restart mon-site
60+dimdams logs mon-bot -f
61+dimdams backup mon-site
62+```
63+
64+## Options
65+
66+| Option | Effet |
67+| --- | --- |
68+| `--json` | Sortie JSON brute, pour vos scripts |
69+| `-h`, `--help` | Aide |
70+| `-v`, `--version` | Version |
71+
72+| Variable d'environnement | Effet |
73+| --- | --- |
74+| `DIMDAMS_API_KEY` | Clé d'API, prioritaire sur la clé enregistrée |
75+| `DIMDAMS_API_URL` | Adresse du panel (`{{MAIN_SITE_URL}}` par défaut) |
76+| `NO_COLOR` | Désactive les couleurs |
Ajoutécontent/api/deploiement-git.md+83−0
@@ -0,0 +1,83 @@
1+---
2+title: Déploiement Git
3+description: Relier un service à un dépôt Git pour le mettre à jour à chaque git push.
4+order: 5
5+---
6+
7+## Récupérer la configuration Git & Webhook
8+
9+`GET /api/hosts/{id}/git` · permission `hosts:read`
10+
11+Renvoie l'URL du dépôt surveillé, la branche, le secret HMAC de signature et l'URL de Webhook à configurer sur GitHub/GitLab.
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant du service |
18+
19+```bash
20+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/git" \
21+ -H "Authorization: Bearer dim_live_…"
22+```
23+
24+Réponse :
25+
26+```json
27+{
28+ "success": true,
29+ "gitDeployment": {
30+ "id": "cly_git_001",
31+ "repoUrl": "https://github.com/mon-orga/mon-api.git",
32+ "branch": "main",
33+ "autoDeploy": true,
34+ "webhookSecret": "sec_8f9c1e4a...",
35+ "webhookUrl": "{{MAIN_SITE_URL}}/api/webhooks/git/vps-1793740523964-d3bw0r",
36+ "buildScript": "npm install && npm run build",
37+ "deployStatus": "DEPLOYED",
38+ "lastCommitHash": "8f1a9b2",
39+ "lastCommitMsg": "feat: optimize database caching layer",
40+ "lastDeployedAt": "2026-09-05T02:15:00.000Z"
41+ }
42+}
43+```
44+
45+## Déclencher un déploiement ou modifier le pipeline
46+
47+`POST /api/hosts/{id}/git` · permission `hosts:write`
48+
49+Permet de configurer le pipeline Git ou de forcer un redéploiement immédiat du dernier commit.
50+
51+**Paramètres d'adresse**
52+
53+| Nom | Type | Requis | Description |
54+| --- | --- | --- | --- |
55+| `id` | string | oui | Identifiant du service |
56+
57+**Corps JSON**
58+
59+| Nom | Type | Requis | Description |
60+| --- | --- | --- | --- |
61+| `action` | string | oui | "configure" (défaut), "trigger" (déploiement immédiat), ou "delete" |
62+| `repoUrl` | string | non | URL du dépôt (pour "configure") |
63+| `branch` | string | non | Branche (pour "configure") |
64+
65+```bash
66+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/git" \
67+ -H "Authorization: Bearer dim_live_…" \
68+ -H "Content-Type: application/json" \
69+ -d '{"action": "trigger"}'
70+```
71+
72+Réponse :
73+
74+```json
75+{
76+ "success": true,
77+ "gitDeployment": {
78+ "deployStatus": "DEPLOYED",
79+ "lastCommitHash": "8f1a9b2",
80+ "lastDeployedAt": "2026-09-05T03:30:00.000Z"
81+ }
82+}
83+```
Ajoutécontent/api/index.md+75−0
@@ -0,0 +1,75 @@
1+---
2+title: API & CLI
3+description: Piloter vos services Dimdam's depuis vos scripts, vos pipelines CI/CD ou un terminal.
4+---
5+
6+L'API REST de Dimdam's permet d'automatiser ce que vous faites dans le tableau de bord : lister vos services, les démarrer ou les arrêter, lire leurs journaux, gérer les sauvegardes, le pare-feu, les ports, les domaines et les alertes. La [CLI `dimdams`](/api/cli/) utilise la même API et la même clé.
7+
8+## URL de base
9+
10+Toutes les routes sont relatives à cette adresse et répondent en JSON :
11+
12+```text
13+{{MAIN_SITE_URL}}/api
14+```
15+
16+## Authentification
17+
18+Créez une clé dans **Paramètres › Clés d'API** de votre espace client, puis envoyez-la dans l'en-tête `Authorization` de chaque requête. Une clé commence toujours par `dim_live_`.
19+
20+```bash
21+curl "{{MAIN_SITE_URL}}/api/hosts" \
22+ -H "Authorization: Bearer dim_live_…"
23+```
24+
25+> [!WARNING]
26+> La clé n'est affichée qu'une fois, à sa création. Gardez-la comme un mot de passe : dans un gestionnaire de secrets ou les secrets de votre CI, jamais dans le code d'un dépôt. Vous pouvez lui donner une date d'expiration, et la révoquer à tout moment depuis la même page.
27+
28+Pour vérifier une clé et voir ses permissions, appelez [`GET /api/user/me`](/api/services/#verifier-une-cle-api).
29+
30+## Permissions
31+
32+Chaque clé ne reçoit que les permissions dont elle a besoin. La permission demandée par chaque route est indiquée dans cette référence.
33+
34+| Permission | Ce qu'elle permet |
35+| --- | --- |
36+| `hosts:read` | Lire vos services, leur état, leurs métriques, leurs journaux et leur configuration |
37+| `hosts:write` | Modifier la configuration : pare-feu, ports, domaines, alertes, déploiement Git, planification des sauvegardes |
38+| `hosts:power` | Démarrer, arrêter et redémarrer vos services |
39+| `backups:read` | Lister les sauvegardes |
40+| `backups:write` | Créer une sauvegarde |
41+| `user:read` | Lire le journal d'audit de votre compte |
42+| `*` | Toutes les permissions |
43+
44+Une clé agit avec les droits de votre compte : elle voit vos services et ceux qu'une équipe partage avec vous, avec les droits que l'équipe vous a donnés.
45+
46+## Limites de débit
47+
48+Jusqu'à **120 requêtes par minute et par clé**. Au-delà, l'API répond `429`, avec le délai à attendre dans le message d'erreur.
49+
50+## Erreurs
51+
52+En cas d'erreur, la réponse contient `success: false` et un message en français :
53+
54+```json
55+{
56+ "success": false,
57+ "error": "Permissions insuffisantes (scope requis : hosts:power)"
58+}
59+```
60+
61+| Code | Signification |
62+| --- | --- |
63+| `400` | Requête invalide : un paramètre manque ou n'a pas le bon format |
64+| `401` | Clé absente, invalide, expirée ou révoquée, ou permission manquante pour cette route |
65+| `403` | Votre compte n'a pas le droit de faire cette action sur ce service (service partagé en lecture seule, par exemple) |
66+| `404` | Service introuvable, ou vous n'y avez pas accès |
67+| `429` | Limite de débit atteinte |
68+| `500` | Erreur de notre côté : réessayez, puis contactez le support si elle continue |
69+
70+## Identifiants des services
71+
72+Chaque service a un identifiant unique, par exemple `vps-1793740523964-d3bw0r` ou `web-1792669078523-xhyuv9`. Il apparaît dans l'adresse de la page du service dans le tableau de bord, et dans le champ `id` de [`GET /api/hosts`](/api/services/#lister-les-services-actifs).
73+
74+> [!NOTE]
75+> Les exemples de réponse de cette référence montrent les champs principaux. Les réponses réelles peuvent en contenir d'autres : ignorez ceux que vous n'utilisez pas.
Ajoutécontent/api/journal-audit.md+45−0
@@ -0,0 +1,45 @@
1+---
2+title: Journal d'audit
3+description: Historique des actions faites sur votre compte et vos services.
4+order: 11
5+---
6+
7+## Consulter le journal d'audit du compte
8+
9+`GET /api/user/audit-logs` · permission `user:read`
10+
11+Retourne les 150 dernières actions effectuées sur votre compte et vos services (adresse IP, agent, état avant et après). Réservé aux clés à accès complet.
12+
13+```bash
14+curl -X GET "{{MAIN_SITE_URL}}/api/user/audit-logs" \
15+ -H "Authorization: Bearer dim_live_…"
16+```
17+
18+Réponse :
19+
20+```json
21+{
22+ "success": true,
23+ "logs": [
24+ {
25+ "id": "cly_audit_999",
26+ "action": "FIREWALL_RULE_CREATE",
27+ "entityType": "SECURITY",
28+ "ipAddress": "194.250.88.12",
29+ "status": "SUCCESS",
30+ "createdAt": "2026-09-05T03:15:00.000Z"
31+ }
32+ ]
33+}
34+```
35+
36+## Journal d'un service
37+
38+`GET /api/hosts/{id}/audit-logs` · permission `hosts:read`
39+
40+Les 100 dernières actions faites sur un service (démarrage, sauvegarde, pare-feu, ports…), les plus récentes en premier, avec leur auteur.
41+
42+```bash
43+curl "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/audit-logs" \
44+ -H "Authorization: Bearer dim_live_…"
45+```
Ajoutécontent/api/metriques.md+87−0
@@ -0,0 +1,87 @@
1+---
2+title: Métriques
3+description: Historique de consommation (CPU, mémoire, réseau) d'un service et de votre compte.
4+order: 3
5+---
6+
7+## Historique des métriques (24h / 7j / 30j)
8+
9+`GET /api/hosts/{id}/stats/history` · permission `hosts:read`
10+
11+Récupère la série temporelle haute résolution des ressources consommées : pourcentage CPU, mémoire RAM (Mo), I/O disque (Go) et débit réseau entrant/sortant (Mo/s).
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant unique du service |
18+
19+**Paramètres de requête**
20+
21+| Nom | Type | Requis | Description |
22+| --- | --- | --- | --- |
23+| `range` | string | non | Période d'analyse : "24h" (défaut), "7d" ou "30d" |
24+
25+```bash
26+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/stats/history?range=7d" \
27+ -H "Authorization: Bearer dim_live_…"
28+```
29+
30+Réponse :
31+
32+```json
33+{
34+ "success": true,
35+ "timeframe": "7d",
36+ "dataPointsCount": 28,
37+ "metrics": [
38+ {
39+ "timestamp": "2026-09-01T12:00:00.000Z",
40+ "cpuPercent": 18.4,
41+ "ramMb": 1280.0,
42+ "ramLimitMb": 4096.0,
43+ "diskGb": 14.8,
44+ "diskLimitGb": 80.0,
45+ "networkRxMbps": 5.2,
46+ "networkTxMbps": 3.8
47+ }
48+ ]
49+}
50+```
51+
52+## Agrégation globale du parc d'instances
53+
54+`GET /api/dashboard/stats/history` · permission `hosts:read`
55+
56+Fournit la vue consolidée de la consommation de ressources sur l'ensemble de vos services pour alimenter vos dashboards de monitoring d'entreprise.
57+
58+**Paramètres de requête**
59+
60+| Nom | Type | Requis | Description |
61+| --- | --- | --- | --- |
62+| `range` | string | non | Période : "24h" \| "7d" \| "30d" |
63+
64+```bash
65+curl -X GET "{{MAIN_SITE_URL}}/api/dashboard/stats/history?range=24h" \
66+ -H "Authorization: Bearer dim_live_…"
67+```
68+
69+Réponse :
70+
71+```json
72+{
73+ "success": true,
74+ "timeframe": "24h",
75+ "activeHostsCount": 5,
76+ "metrics": [
77+ {
78+ "timestamp": "2026-09-05T00:00:00.000Z",
79+ "avgCpu": 22.1,
80+ "totalRamMb": 6450.0,
81+ "totalDiskGb": 48.5,
82+ "totalNetworkRxMbps": 18.4,
83+ "totalNetworkTxMbps": 12.6
84+ }
85+ ]
86+}
87+```
Ajoutécontent/api/pare-feu.md+122−0
@@ -0,0 +1,122 @@
1+---
2+title: Pare-feu
3+description: Règles de filtrage des connexions entrantes d'un service.
4+order: 4
5+---
6+
7+## Lister les règles de sécurité
8+
9+`GET /api/hosts/{id}/firewall` · permission `hosts:read`
10+
11+Renvoie toutes les règles de filtrage réseau (ACLs) appliquées au niveau matériel et noyau (iptables/nftables) sur le serveur.
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant du service |
18+
19+```bash
20+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/firewall" \
21+ -H "Authorization: Bearer dim_live_…"
22+```
23+
24+Réponse :
25+
26+```json
27+{
28+ "success": true,
29+ "rules": [
30+ {
31+ "id": "cly123456789",
32+ "direction": "INBOUND",
33+ "action": "ACCEPT",
34+ "protocol": "TCP",
35+ "portRange": "22",
36+ "sourceCidr": "194.250.0.0/16",
37+ "description": "Accès SSH VPN Entreprise",
38+ "priority": 10,
39+ "enabled": true
40+ }
41+ ]
42+}
43+```
44+
45+## Créer une règle de filtrage à chaud
46+
47+`POST /api/hosts/{id}/firewall` · permission `hosts:write`
48+
49+Injecte instantanément une règle de pare-feu appliquée en temps réel sur la pile réseau sans redémarrage de la machine.
50+
51+**Paramètres d'adresse**
52+
53+| Nom | Type | Requis | Description |
54+| --- | --- | --- | --- |
55+| `id` | string | oui | Identifiant du service |
56+
57+**Corps JSON**
58+
59+| Nom | Type | Requis | Description |
60+| --- | --- | --- | --- |
61+| `direction` | string | oui | "INBOUND" (entrant) ou "OUTBOUND" (sortant) |
62+| `action` | string | oui | "ACCEPT" (autoriser) ou "DROP" (rejeter) |
63+| `protocol` | string | oui | "TCP" \| "UDP" \| "ICMP" \| "ANY" |
64+| `portRange` | string | non | Port unique (ex: "8080") ou plage (ex: "3000-3050") |
65+| `sourceCidr` | string | non | Masque CIDR autorisé (défaut: "0.0.0.0/0") |
66+| `description` | string | non | Note explicative pour l'équipe |
67+| `priority` | number | non | Priorité d'évaluation (défaut: 100) |
68+
69+```bash
70+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/firewall" \
71+ -H "Authorization: Bearer dim_live_…" \
72+ -H "Content-Type: application/json" \
73+ -d '{
74+ "direction": "INBOUND",
75+ "action": "ACCEPT",
76+ "protocol": "TCP",
77+ "portRange": "443",
78+ "sourceCidr": "0.0.0.0/0",
79+ "description": "Port HTTPS Web"
80+ }'
81+```
82+
83+Réponse :
84+
85+```json
86+{
87+ "success": true,
88+ "rule": {
89+ "id": "cly987654321",
90+ "direction": "INBOUND",
91+ "action": "ACCEPT",
92+ "protocol": "TCP",
93+ "portRange": "443",
94+ "sourceCidr": "0.0.0.0/0",
95+ "description": "Port HTTPS Web",
96+ "priority": 100,
97+ "enabled": true
98+ }
99+}
100+```
101+
102+## Modifier une règle
103+
104+`PUT /api/hosts/{id}/firewall/{ruleId}` · permission `hosts:write`
105+
106+Mêmes champs que la création, tous facultatifs : seuls les champs envoyés changent. La règle est appliquée tout de suite.
107+
108+```bash
109+curl -X PUT "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/firewall/RULE_ID" \
110+ -H "Authorization: Bearer dim_live_…" \
111+ -H "Content-Type: application/json" \
112+ -d '{"sourceCidr": "203.0.113.0/24"}'
113+```
114+
115+## Supprimer une règle
116+
117+`DELETE /api/hosts/{id}/firewall/{ruleId}` · permission `hosts:write`
118+
119+```bash
120+curl -X DELETE "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/firewall/RULE_ID" \
121+ -H "Authorization: Bearer dim_live_…"
122+```
Ajoutécontent/api/reseau.md+143−0
@@ -0,0 +1,143 @@
1+---
2+title: Réseau et ports
3+description: Ports ouverts et adresses d'un VPS.
4+order: 6
5+---
6+
7+## Lister les ports ouverts et sous-domaines
8+
9+`GET /api/hosts/{id}/ports` · permission `hosts:read`
10+
11+Retourne les ports TCP/UDP ouverts sur le VPS, les sous-domaines routés ainsi que les quotas gratuits et suppléments.
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant du service VPS |
18+
19+```bash
20+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/ports" \
21+ -H "Authorization: Bearer dim_live_…"
22+```
23+
24+Réponse :
25+
26+```json
27+{
28+ "success": true,
29+ "ports": [
30+ {
31+ "id": "port_172839485",
32+ "containerPort": 4000,
33+ "protocol": "tcp",
34+ "hostPort": 32775,
35+ "label": "LiteLLM API",
36+ "isFree": true
37+ }
38+ ],
39+ "subdomains": [
40+ {
41+ "id": "sub_172839485",
42+ "subdomain": "mon-ia",
43+ "targetPort": 4000,
44+ "ssl": true,
45+ "isFree": true
46+ }
47+ ],
48+ "summary": {
49+ "totalPorts": 1,
50+ "freePorts": 3,
51+ "billablePorts": 0,
52+ "totalSubdomains": 1,
53+ "freeSubdomains": 1,
54+ "billableSubdomains": 0,
55+ "totalMonthlyCostEUR": 0
56+ }
57+}
58+```
59+
60+## Ouvrir un port réseau (TCP / UDP)
61+
62+`POST /api/hosts/{id}/ports` · permission `hosts:write`
63+
64+Ouvre un port interne sur le VPS et lui attribue dynamiquement un port hôte public accessible depuis Internet.
65+
66+**Paramètres d'adresse**
67+
68+| Nom | Type | Requis | Description |
69+| --- | --- | --- | --- |
70+| `id` | string | oui | Identifiant du service VPS |
71+
72+**Corps JSON**
73+
74+| Nom | Type | Requis | Description |
75+| --- | --- | --- | --- |
76+| `containerPort` | number | oui | Port interne (1 - 65535) |
77+| `protocol` | string | non | 'tcp' ou 'udp' (défaut: 'tcp') |
78+| `label` | string | non | Nom ou description du service |
79+
80+```bash
81+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/ports" \
82+ -H "Authorization: Bearer dim_live_…" \
83+ -H "Content-Type: application/json" \
84+ -d '{"containerPort": 4000, "protocol": "tcp", "label": "LiteLLM API"}'
85+```
86+
87+Réponse :
88+
89+```json
90+{
91+ "success": true,
92+ "port": {
93+ "id": "port_172839485",
94+ "containerPort": 4000,
95+ "protocol": "tcp",
96+ "hostPort": 32775,
97+ "label": "LiteLLM API",
98+ "isFree": true
99+ },
100+ "message": "Port 4000/TCP ouvert avec succès"
101+}
102+```
103+
104+## Lier un domaine personnalisé à un port
105+
106+`POST /api/hosts/{id}/custom-domains` · permission `hosts:write`
107+
108+Relie votre propre domaine à un port web (HTTP) ouvert du VPS, avec certificat SSL automatique. Le domaine doit d'abord pointer vers Dimdam's : un enregistrement CNAME vers `connect.dimdams.com`. Voir [Adresses et domaines](/hebergement/adresses-et-domaines/).
109+
110+**Corps JSON**
111+
112+| Nom | Type | Requis | Description |
113+| --- | --- | --- | --- |
114+| `domain` | string | oui | Le domaine, sans `https://` (ex. `app.exemple.fr`) |
115+| `targetPort` | number | oui | Port web ouvert du VPS vers lequel envoyer le trafic |
116+
117+```bash
118+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/custom-domains" \
119+ -H "Authorization: Bearer dim_live_…" \
120+ -H "Content-Type: application/json" \
121+ -d '{"domain": "app.exemple.fr", "targetPort": 4000}'
122+```
123+
124+Réponse :
125+
126+```json
127+{
128+ "success": true,
129+ "customDomain": { "id": "cd_1793812345678_k2j9x", "domain": "app.exemple.fr", "targetPort": 4000, "status": "active", "ssl": true }
130+}
131+```
132+
133+## Vérifier un domaine personnalisé
134+
135+`GET /api/hosts/{id}/custom-domains/{customDomainId}` · permission `hosts:read`
136+
137+Contrôle de nouveau le DNS et redemande le certificat si besoin (utile quand le statut est « en attente »).
138+
139+## Retirer un domaine personnalisé
140+
141+`DELETE /api/hosts/{id}/custom-domains/{customDomainId}` · permission `hosts:write`
142+
143+Supprime le routage et le certificat du domaine.
Ajoutécontent/api/sauvegardes.md+119−0
@@ -0,0 +1,119 @@
1+---
2+title: Sauvegardes
3+description: Lister, créer et planifier les sauvegardes d'un service.
4+order: 9
5+---
6+
7+## Lister les sauvegardes
8+
9+`GET /api/hosts/{id}/backups` · permission `backups:read`
10+
11+Retourne les sauvegardes du service, manuelles et automatiques, de la plus récente à la plus ancienne.
12+
13+**Paramètres d'adresse**
14+
15+| Nom | Type | Requis | Description |
16+| --- | --- | --- | --- |
17+| `id` | string | oui | Identifiant du service |
18+
19+```bash
20+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/backups" \
21+ -H "Authorization: Bearer dim_live_…"
22+```
23+
24+Réponse :
25+
26+```json
27+{
28+ "success": true,
29+ "backups": [
30+ {
31+ "name": "auto_2026-09-04T03-00-00.tar.gz",
32+ "size": 145892034,
33+ "created": "2026-09-04T03:00:12.000Z",
34+ "type": "auto"
35+ }
36+ ]
37+}
38+```
39+
40+## Créer une sauvegarde manuelle
41+
42+`POST /api/hosts/{id}/backups` · permission `backups:write`
43+
44+Lance une sauvegarde immédiate des données du service. Au plus 10 sauvegardes manuelles, et une toutes les 10 minutes.
45+
46+**Paramètres d'adresse**
47+
48+| Nom | Type | Requis | Description |
49+| --- | --- | --- | --- |
50+| `id` | string | oui | Identifiant du service |
51+
52+**Corps JSON**
53+
54+| Nom | Type | Requis | Description |
55+| --- | --- | --- | --- |
56+| `action` | string | oui | "create" |
57+
58+```bash
59+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/backups" \
60+ -H "Authorization: Bearer dim_live_…" \
61+ -H "Content-Type: application/json" \
62+ -d '{"action": "create"}'
63+```
64+
65+Réponse :
66+
67+```json
68+{
69+ "success": true,
70+ "backup": {
71+ "name": "manual_2026-09-05T14-21-07.tar.gz",
72+ "size": 145903311,
73+ "type": "manual"
74+ }
75+}
76+```
77+
78+## Consulter la planification
79+
80+`GET /api/hosts/{id}/backups/schedule` · permission `hosts:read`
81+
82+Retourne la planification des sauvegardes automatiques du service (`schedule` vaut `null` si aucune n'est enregistrée).
83+
84+```bash
85+curl "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/backups/schedule" \
86+ -H "Authorization: Bearer dim_live_…"
87+```
88+
89+## Programmer les sauvegardes automatiques
90+
91+`PUT /api/hosts/{id}/backups/schedule` · permission `hosts:write`
92+
93+Crée ou remplace la planification. Il faut aussi avoir le droit de gérer les sauvegardes du service (propriétaire, ou membre d'équipe avec ce droit).
94+
95+**Corps JSON**
96+
97+| Nom | Type | Requis | Description |
98+| --- | --- | --- | --- |
99+| `enabled` | boolean | non | Active ou suspend la planification (défaut : `true`) |
100+| `frequency` | string | non | `"DAILY"` (chaque jour, défaut) ou `"WEEKLY"` |
101+| `executionHour` | number | non | Heure de lancement, de 0 à 23, en UTC (défaut : 3) |
102+| `retentionDaily` | number | non | Sauvegardes quotidiennes gardées, de 1 à 30 (défaut : 7) |
103+| `retentionWeekly` | number | non | Sauvegardes hebdomadaires gardées, de 1 à 12 (défaut : 4) |
104+
105+```bash
106+curl -X PUT "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/backups/schedule" \
107+ -H "Authorization: Bearer dim_live_…" \
108+ -H "Content-Type: application/json" \
109+ -d '{"frequency": "DAILY", "executionHour": 3, "retentionDaily": 7}'
110+```
111+
112+Réponse :
113+
114+```json
115+{
116+ "success": true,
117+ "schedule": { "enabled": true, "frequency": "DAILY", "executionHour": 3, "retentionDaily": 7, "retentionWeekly": 4, "nextRunAt": "2026-11-05T03:00:00.000Z" }
118+}
119+```
Ajoutécontent/api/services.md+173−0
@@ -0,0 +1,173 @@
1+---
2+title: Services
3+description: Lister vos services, les consulter, les démarrer ou les arrêter, lire leurs journaux.
4+order: 2
5+---
6+
7+## Lister les services actifs
8+
9+`GET /api/hosts` · permission `hosts:read`
10+
11+Retourne vos services et ceux qu'une équipe partage avec vous, avec leur état et leur consommation du moment. Avec une clé d'API, la configuration est renvoyée sans aucun secret (mots de passe, jetons).
12+
13+```bash
14+curl "{{MAIN_SITE_URL}}/api/hosts" \
15+ -H "Authorization: Bearer dim_live_…"
16+```
17+
18+Réponse (champs principaux) :
19+
20+```json
21+{
22+ "success": true,
23+ "hosts": [
24+ {
25+ "id": "vps-1793740523964-d3bw0r",
26+ "type": "vps",
27+ "subtype": "ubuntu-24.04",
28+ "name": "Xenon",
29+ "status": "running",
30+ "initialized": true,
31+ "createdAt": "2026-11-03T18:02:11.000Z",
32+ "isShared": false,
33+ "cpu_usage": 3.4,
34+ "ram_usage": 21.7,
35+ "memory_usage": 1863270400,
36+ "memory_limit": 8589934592,
37+ "domain": null
38+ }
39+ ],
40+ "stats": { "totalHosts": 4, "runningHosts": 3, "configuredHosts": 4, "unconfiguredHosts": 0 }
41+}
42+```
43+
44+## Détails d'un serveur
45+
46+`GET /api/hosts/{id}` · permission `hosts:read`
47+
48+Récupère les métadonnées détaillées, l'état de santé temps réel et la configuration d'un service spécifique.
49+
50+**Paramètres d'adresse**
51+
52+| Nom | Type | Requis | Description |
53+| --- | --- | --- | --- |
54+| `id` | string | oui | Identifiant unique du service (ex: vps-1793740523964-d3bw0r) |
55+
56+```bash
57+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r" \
58+ -H "Authorization: Bearer dim_live_…"
59+```
60+
61+Réponse :
62+
63+```json
64+{
65+ "success": true,
66+ "host": {
67+ "id": "vps-1793740523964-d3bw0r",
68+ "name": "Production-VPS-01",
69+ "type": "vps",
70+ "status": "active",
71+ "port": 22,
72+ "domain": "vps-01.dimdams.net",
73+ "cpu_usage": 14.2,
74+ "memory_usage": 1024,
75+ "memory_limit": 4096,
76+ "disk_usage": 12.5,
77+ "disk_limit": 80.0
78+ }
79+}
80+```
81+
82+## Contrôle de puissance (Start / Stop / Restart)
83+
84+`POST /api/hosts/{id}/actions` · permission `hosts:power`
85+
86+Démarre, arrête ou redémarre l'instance. Avec une clé API, seules ces trois actions sont permises : l'initialisation et la réinitialisation se font depuis le tableau de bord.
87+
88+**Paramètres d'adresse**
89+
90+| Nom | Type | Requis | Description |
91+| --- | --- | --- | --- |
92+| `id` | string | oui | Identifiant unique du service |
93+
94+**Corps JSON**
95+
96+| Nom | Type | Requis | Description |
97+| --- | --- | --- | --- |
98+| `action` | string | oui | Action à exécuter : "start" \| "stop" \| "restart" |
99+
100+```bash
101+curl -X POST "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/actions" \
102+ -H "Authorization: Bearer dim_live_…" \
103+ -H "Content-Type: application/json" \
104+ -d '{"action": "restart"}'
105+```
106+
107+Réponse :
108+
109+```json
110+{
111+ "success": true
112+}
113+```
114+
115+## Lire les logs
116+
117+`GET /api/hosts/{id}/logs` · permission `hosts:read`
118+
119+Retourne les dernières lignes des logs du service : sortie du conteneur (docker) ou, pour les serveurs web, journaux d'accès et d'erreurs.
120+
121+**Paramètres d'adresse**
122+
123+| Nom | Type | Requis | Description |
124+| --- | --- | --- | --- |
125+| `id` | string | oui | Identifiant unique du service |
126+
127+**Paramètres de requête**
128+
129+| Nom | Type | Requis | Description |
130+| --- | --- | --- | --- |
131+| `type` | string | non | "docker" (défaut) \| "access" \| "error" |
132+| `lines` | number | non | Nombre de lignes, de 1 à 1000 (défaut : 100) |
133+
134+```bash
135+curl -X GET "{{MAIN_SITE_URL}}/api/hosts/vps-1793740523964-d3bw0r/logs?lines=200" \
136+ -H "Authorization: Bearer dim_live_…"
137+```
138+
139+Réponse :
140+
141+```json
142+{
143+ "success": true,
144+ "type": "docker",
145+ "logs": "Server listening on port 3000\nGET / 200 4ms"
146+}
147+```
148+
149+## Vérifier une clé API
150+
151+`GET /api/user/me` · permission `aucun`
152+
153+Retourne le compte associé à la clé, ses permissions et sa date d'expiration. Aucune permission particulière n'est requise.
154+
155+```bash
156+curl -X GET "{{MAIN_SITE_URL}}/api/user/me" \
157+ -H "Authorization: Bearer dim_live_…"
158+```
159+
160+Réponse :
161+
162+```json
163+{
164+ "success": true,
165+ "user": { "id": 42, "username": "alice", "email": "alice@example.com" },
166+ "apiKey": {
167+ "name": "CI",
168+ "prefix": "dim_live_9f8e7d6c...",
169+ "scopes": ["hosts:read", "hosts:power"],
170+ "expiresAt": null
171+ }
172+}
173+```
Ajoutécontent/git/api.md+53−0
@@ -0,0 +1,53 @@
1+---
2+title: API
3+description: Automatiser Dimdam's Git avec son API REST.
4+order: 6
5+---
6+
7+Dimdam's Git a sa propre API REST, différente de l'[API du panel](/api/). Elle permet de gérer les dépôts, les tickets, les demandes de fusion, les versions, les organisations, les webhooks…
8+
9+## URL de base
10+
11+```text
12+{{GIT_URL}}/api/v1
13+```
14+
15+La liste complète des routes, avec un formulaire pour les essayer, est sur [{{GIT_URL}}/api/swagger]({{GIT_URL}}/api/swagger).
16+
17+## Authentification
18+
19+Créez un [jeton d'accès](/git/premiers-pas/#jetons-dacces) avec les droits nécessaires, puis envoyez-le dans l'en-tête `Authorization` :
20+
21+```bash
22+curl "{{GIT_URL}}/api/v1/user" \
23+ -H "Authorization: token VOTRE_JETON"
24+```
25+
26+## Exemples
27+
28+Lister vos dépôts :
29+
30+```bash
31+curl "{{GIT_URL}}/api/v1/user/repos" \
32+ -H "Authorization: token VOTRE_JETON"
33+```
34+
35+Créer un dépôt privé :
36+
37+```bash
38+curl -X POST "{{GIT_URL}}/api/v1/user/repos" \
39+ -H "Authorization: token VOTRE_JETON" \
40+ -H "Content-Type: application/json" \
41+ -d '{"name": "mon-projet", "private": true}'
42+```
43+
44+Ouvrir un ticket :
45+
46+```bash
47+curl -X POST "{{GIT_URL}}/api/v1/repos/votre-compte/mon-projet/issues" \
48+ -H "Authorization: token VOTRE_JETON" \
49+ -H "Content-Type: application/json" \
50+ -d '{"title": "La page d’accueil ne charge pas", "body": "Étapes pour reproduire…"}'
51+```
52+
53+Les listes sont paginées avec `?page=` et `?limit=` (50 éléments au plus par page).
Ajoutécontent/git/automatisation.md+58−0
@@ -0,0 +1,58 @@
1+---
2+title: CI et webhooks
3+description: Lancer des tâches à chaque push, et prévenir vos outils des événements d'un dépôt.
4+order: 3
5+---
6+
7+## CI
8+
9+La CI lance des tâches à chaque `push`, à chaque demande de fusion ou à la main : tests, construction, publication. Les workflows s'écrivent en YAML dans le dossier `.github/workflows/` du dépôt, avec la même syntaxe que GitHub Actions.
10+
11+```yaml
12+# .github/workflows/tests.yml
13+name: Tests
14+on:
15+ push:
16+ pull_request:
17+jobs:
18+ tests:
19+ runs-on: ubuntu-latest
20+ steps:
21+ - uses: actions/checkout@v4
22+ - uses: actions/setup-node@v4
23+ with:
24+ node-version: 22
25+ - run: npm ci
26+ - run: npm test
27+```
28+
29+Suivez les exécutions dans l'onglet **Actions** du dépôt : journaux en direct, annulation, relance, lancement manuel avec paramètres.
30+
31+### Secrets et variables
32+
33+Ajoutez vos secrets (jetons, clés d'API) dans **Paramètres › CI** du dépôt ou de l'organisation, et utilisez-les avec `${{ secrets.MON_SECRET }}`. Ils n'apparaissent jamais en clair dans les journaux.
34+
35+### Cache
36+
37+`actions/cache` garde vos dépendances d'une exécution à l'autre, pour aller plus vite. Le cache est propre à chaque dépôt.
38+
39+### Limites
40+
41+| | |
42+| --- | --- |
43+| Ressources d'une tâche | 2 processeurs, 4 Go de mémoire |
44+| Durée d'une tâche | 60 minutes au plus |
45+| Minutes par mois | 500 offertes, 2 000 avec Git Pro |
46+| Artefacts | Conservés 14 jours |
47+
48+Les tâches tournent dans des conteneurs jetables, isolés du réseau interne de Dimdam's. À la limite du mois, la CI des dépôts du compte s'arrête jusqu'au mois suivant (les tâches déjà lancées se terminent).
49+
50+## Webhooks
51+
52+Un webhook envoie une requête HTTP à votre outil à chaque événement d'un dépôt (push, ticket, demande de fusion, version publiée, exécution de CI…). Ajoutez-le dans **Paramètres › Webhooks** du dépôt ou de l'organisation.
53+
54+Chaque envoi est signé avec votre secret (HMAC) : vérifiez la signature avant de faire confiance au contenu. Les en-têtes sont compatibles avec ceux de GitHub, ce qui permet d'utiliser la plupart des outils existants.
55+
56+## Déployer sur un service Dimdam's
57+
58+Pour mettre à jour un site ou un bot Dimdam's à chaque `push`, voir [Déploiement Git](/hebergement/deploiement-git/).
Ajoutécontent/git/collaboration.md+33−0
@@ -0,0 +1,33 @@
1+---
2+title: Collaboration
3+description: Tickets, demandes de fusion, revues, organisations et équipes.
4+order: 2
5+---
6+
7+## Organisations et équipes
8+
9+Une **organisation** regroupe les dépôts d'un projet ou d'une entreprise. Ses membres sont répartis en **équipes**, avec des droits sur les dépôts (lecture, écriture, administration). Vous pouvez aussi ajouter des **collaborateurs** directement à un dépôt.
10+
11+## Tickets
12+
13+Les tickets servent à suivre les bugs et les tâches, avec des étiquettes, des jalons et des personnes assignées. Une conversation peut être verrouillée.
14+
15+## Demandes de fusion
16+
17+Une demande de fusion propose de fusionner une branche dans une autre :
18+
19+- comparez les branches et ouvrez la demande depuis la page de comparaison ;
20+- relisez les différences, commentez une ligne ou un bloc de lignes (Maj + clic), approuvez ou demandez des modifications ;
21+- fusionnez par un commit de fusion, un « squash » ou un « rebase ».
22+
23+## Branches protégées
24+
25+Protégez une branche (par exemple `main`) dans **Paramètres › Branches** : interdiction de pousser directement, approbations obligatoires, vérifications de la CI avant la fusion.
26+
27+## Notifications
28+
29+Dimdam's Git vous prévient des mentions, des assignations et de l'activité des dépôts que vous suivez, dans l'application et par e-mail. Réglez ce que vous recevez dans **Paramètres › Notifications & e-mails**.
30+
31+## Wiki
32+
33+Chaque dépôt peut avoir un wiki en Markdown, avec des liens `[[Page]]` et son propre historique. Il se clone comme un dépôt : `mon-projet.wiki.git`.
Ajoutécontent/git/index.md+27−0
@@ -0,0 +1,27 @@
1+---
2+title: Dimdam's Git
3+description: Héberger vos dépôts Git en France : code, collaboration, CI, paquets et sites statiques.
4+---
5+
6+[Dimdam's Git]({{GIT_URL}}) héberge vos dépôts sur les serveurs de Dimdam's, en France. On s'y connecte avec son compte Dimdam's, et on y trouve tout ce qu'il faut pour travailler à plusieurs :
7+
8+- **le code :** dépôts publics ou privés, branches, historique, recherche, éditeur en ligne, import depuis GitHub ou GitLab ;
9+- **la collaboration :** tickets, demandes de fusion avec revue ligne par ligne, branches protégées, organisations et équipes ;
10+- **l'automatisation :** CI à chaque `push`, webhooks, déploiement sur vos services Dimdam's ;
11+- **la publication :** versions publiées, registre de paquets et de conteneurs, sites statiques avec Pages.
12+
13+## Premiers pas
14+
15+1. Connectez-vous sur [{{GIT_URL}}]({{GIT_URL}}) avec votre compte Dimdam's : votre compte Git est créé à la première connexion.
16+2. Ajoutez votre clé SSH (voir [Premiers pas](/git/premiers-pas/#cles-ssh)).
17+3. Créez un dépôt, ou importez-en un depuis GitHub, GitLab ou un autre serveur Git.
18+
19+## Offre
20+
21+| | Gratuit | Dimdam's Git Pro |
22+| --- | --- | --- |
23+| Espace | 1 Go | 5 à 500 Go en plus |
24+| Dépôts | 25 | Sans limite |
25+| CI | 500 minutes par mois | 2 000 minutes par mois |
26+
27+Git Pro s'achète dans la [boutique]({{MAIN_SITE_URL}}/shop) et s'applique automatiquement. Un compte Git Pro peut faire profiter de son offre jusqu'à 3 de ses organisations.
Ajoutécontent/git/pages.md+58−0
@@ -0,0 +1,58 @@
1+---
2+title: Pages
3+description: Publier un site statique depuis un dépôt, avec HTTPS et votre propre domaine.
4+order: 4
5+---
6+
7+Pages publie un site statique (HTML, CSS, JavaScript, images) depuis un de vos dépôts, gratuitement, avec un certificat HTTPS automatique.
8+
9+## Publier un site
10+
11+Deux façons de faire :
12+
13+- **un dépôt nommé `pages` :** son contenu est publié à l'adresse `https://<compte>.pages.dimdams.net` ;
14+- **une branche `pages` dans n'importe quel dépôt :** son contenu est publié à l'adresse `https://<compte>.pages.dimdams.net/<dépôt>/`.
15+
16+Le fichier `index.html` à la racine sert de page d'accueil, et un `404.html` remplace la page d'erreur par défaut.
17+
18+## Construire le site avec la CI
19+
20+Si votre site se construit (Vite, Astro, Hugo…), laissez la [CI](/git/automatisation/) pousser le résultat dans la branche `pages` à chaque `push` sur `main` :
21+
22+```yaml
23+# .github/workflows/pages.yml
24+name: Pages
25+on:
26+ push:
27+ branches: [main]
28+jobs:
29+ publier:
30+ runs-on: ubuntu-latest
31+ steps:
32+ - uses: actions/checkout@v4
33+ - uses: actions/setup-node@v4
34+ with:
35+ node-version: 22
36+ - run: npm ci && npm run build
37+ - name: Pousser dans la branche pages
38+ run: |
39+ cd dist
40+ git init -b pages
41+ git add -A
42+ git -c user.name=ci -c user.email=ci@localhost commit -m "Publication"
43+ git push -f "https://ci:${{ secrets.PAGES_TOKEN }}@git.dimdams.com/${{ github.repository }}.git" pages
44+```
45+
46+`PAGES_TOKEN` est un [jeton d'accès](/git/premiers-pas/#jetons-dacces) avec le droit d'écriture sur le dépôt, ajouté dans les secrets du dépôt.
47+
48+## Domaine personnalisé
49+
50+1. Ajoutez votre domaine dans **Paramètres › Pages** du dépôt.
51+2. Chez votre registraire, faites-le pointer vers Pages :
52+ - pour un sous-domaine (`www.exemple.fr`), un enregistrement **CNAME** vers `<compte>.pages.dimdams.net` ;
53+ - pour un domaine racine (`exemple.fr`), l'enregistrement **A** indiqué dans les paramètres ;
54+ - et dans les deux cas, l'enregistrement **TXT** `_dimdams-pages` indiqué, qui prouve que le domaine est à vous.
55+3. Une fois le DNS propagé, le certificat HTTPS est créé automatiquement.
56+
57+> [!NOTE]
58+> Les noms en `dimdams.com` sont réservés à Dimdam's et ne peuvent pas servir de domaine personnalisé.
Ajoutécontent/git/paquets.md+80−0
@@ -0,0 +1,80 @@
1+---
2+title: Paquets et conteneurs
3+description: Publier des images de conteneur et des paquets npm, PyPI ou Maven.
4+order: 5
5+---
6+
7+Chaque compte et chaque organisation a son registre de paquets. Les paquets apparaissent dans l'onglet **Paquets** du profil, et peuvent être liés à un dépôt. Pour vous authentifier, utilisez votre nom d'utilisateur et un [jeton d'accès](/git/premiers-pas/#jetons-dacces) avec les droits sur les paquets.
8+
9+## Images de conteneur
10+
11+```bash
12+docker login git.dimdams.com
13+docker build -t git.dimdams.com/votre-compte/mon-app:1.0 .
14+docker push git.dimdams.com/votre-compte/mon-app:1.0
15+```
16+
17+Et pour la récupérer, par exemple sur un VPS :
18+
19+```bash
20+docker pull git.dimdams.com/votre-compte/mon-app:1.0
21+```
22+
23+## npm
24+
25+```bash
26+npm config set @votre-compte:registry {{GIT_URL}}/api/packages/votre-compte/npm/
27+npm config set -- "//git.dimdams.com/api/packages/votre-compte/npm/:_authToken" "VOTRE_JETON"
28+npm publish
29+```
30+
31+Le nom du paquet doit commencer par `@votre-compte/` dans `package.json`.
32+
33+## PyPI
34+
35+```bash
36+python -m twine upload \
37+ --repository-url {{GIT_URL}}/api/packages/votre-compte/pypi \
38+ -u votre-compte -p VOTRE_JETON dist/*
39+```
40+
41+Pour l'installer :
42+
43+```bash
44+pip install --index-url {{GIT_URL}}/api/packages/votre-compte/pypi/simple/ mon-paquet
45+```
46+
47+## Maven
48+
49+Dans `pom.xml` :
50+
51+```xml
52+<distributionManagement>
53+ <repository>
54+ <id>dimdams</id>
55+ <url>{{GIT_URL}}/api/packages/votre-compte/maven</url>
56+ </repository>
57+</distributionManagement>
58+```
59+
60+Et dans `~/.m2/settings.xml`, le jeton associé à l'identifiant `dimdams` :
61+
62+```xml
63+<servers>
64+ <server>
65+ <id>dimdams</id>
66+ <configuration>
67+ <httpHeaders>
68+ <property>
69+ <name>Authorization</name>
70+ <value>token VOTRE_JETON</value>
71+ </property>
72+ </httpHeaders>
73+ </configuration>
74+ </server>
75+</servers>
76+```
77+
78+## Espace utilisé
79+
80+Les paquets comptent dans l'espace de votre compte (1 Go offert, plus avec Git Pro). Supprimez les anciennes versions depuis la page du paquet pour libérer de la place.
Ajoutécontent/git/premiers-pas.md+62−0
@@ -0,0 +1,62 @@
1+---
2+title: Premiers pas
3+description: Se connecter, ajouter une clé SSH, cloner et pousser.
4+order: 1
5+---
6+
7+## Se connecter
8+
9+Dimdam's Git utilise votre compte Dimdam's : cliquez sur **Se connecter** sur [{{GIT_URL}}]({{GIT_URL}}). Votre compte Git est créé à la première connexion.
10+
11+## Clés SSH
12+
13+Pour cloner et pousser en SSH sans mot de passe, ajoutez votre clé publique dans **Paramètres › Clés SSH**.
14+
15+Vous n'avez pas encore de clé ? Créez-en une :
16+
17+```bash
18+ssh-keygen -t ed25519 -C "vous@exemple.fr"
19+cat ~/.ssh/id_ed25519.pub
20+```
21+
22+Copiez la ligne affichée dans Dimdam's Git, puis testez :
23+
24+```bash
25+ssh -T git@git.dimdams.com
26+```
27+
28+## Cloner et pousser
29+
30+En SSH (recommandé) :
31+
32+```bash
33+git clone git@git.dimdams.com:votre-compte/mon-projet.git
34+```
35+
36+En HTTPS, avec un [jeton d'accès](#jetons-dacces) comme mot de passe :
37+
38+```bash
39+git clone {{GIT_URL}}/votre-compte/mon-projet.git
40+```
41+
42+Pour envoyer un projet existant dans un nouveau dépôt :
43+
44+```bash
45+git remote add origin git@git.dimdams.com:votre-compte/mon-projet.git
46+git push -u origin main
47+```
48+
49+## Jetons d'accès
50+
51+Les jetons d'accès servent à cloner et pousser en HTTPS, à appeler l'[API](/git/api/) et à utiliser le [registre de paquets](/git/paquets/). Créez-les dans **Paramètres › Jetons d'accès**, avec seulement les droits nécessaires.
52+
53+> [!WARNING]
54+> Un jeton n'est affiché qu'une fois. Traitez-le comme un mot de passe, et révoquez-le s'il a pu fuiter.
55+
56+## Clés GPG
57+
58+Ajoutez votre clé GPG dans **Paramètres › Clés GPG** : vos commits signés apparaîtront comme vérifiés.
59+
60+## Importer un dépôt
61+
62+**Nouveau dépôt › Importer** récupère un dépôt depuis GitHub, GitLab ou n'importe quelle adresse Git. Vous pouvez le garder synchronisé (miroir) toutes les heures, 8 heures ou 24 heures.
Ajoutécontent/hebergement/adresses-et-domaines.md+45−0
@@ -0,0 +1,45 @@
1+---
2+title: Adresses et domaines
3+description: L'adresse fournie avec chaque service, et comment utiliser votre propre nom de domaine.
4+order: 6
5+---
6+
7+## L'adresse de votre service
8+
9+Chaque service reçoit une adresse en `dimdams.net`, avec un certificat HTTPS automatique :
10+
11+| Service | Adresse |
12+| --- | --- |
13+| Site web | `votre-nom.dimdams.net` |
14+| Serveur de jeu | `votre-nom.play.dimdams.net`, avec le port du jeu |
15+| VPS | `votre-nom.vps.dimdams.net` (sous-domaine lié à un port) |
16+
17+> [!NOTE]
18+> Les adresses créées avant novembre 2026 en `dimdams.com` continuent de fonctionner.
19+
20+## Utiliser votre propre domaine
21+
22+Vous pouvez relier un domaine acheté chez n'importe quel registraire (OVH, Gandi, Cloudflare…) à votre service.
23+
24+### 1. Faire pointer le domaine vers Dimdam's
25+
26+Chez votre registraire, créez un enregistrement DNS :
27+
28+| Type | Nom | Cible |
29+| --- | --- | --- |
30+| `CNAME` | `app` (pour `app.exemple.fr`) | `connect.dimdams.com` |
31+
32+Pour un VPS, vous pouvez aussi ajouter un enregistrement `AAAA` vers son adresse IPv6 dédiée (onglet **Réseau**).
33+
34+> [!WARNING]
35+> Si votre DNS est chez Cloudflare, mettez l'enregistrement en « DNS uniquement » (nuage gris), sans proxy. La propagation peut prendre de quelques minutes à quelques heures.
36+
37+### 2. Lier le domaine dans le tableau de bord
38+
39+- **Site web :** dans la page du service, section domaine personnalisé.
40+- **VPS :** dans **Réseau**, bouton **Domaine Personnalisé**. Choisissez le port web de destination et entrez le domaine. Pas besoin de créer de sous-domaine.
41+
42+Dimdam's vérifie le DNS, puis obtient le certificat HTTPS (Let's Encrypt). Si le DNS n'est pas encore propagé, le domaine reste « en attente » : le bouton de vérification relance le contrôle.
43+
44+> [!NOTE]
45+> Les domaines en `dimdams.com` et `dimdams.net` ne peuvent pas être utilisés comme domaine personnalisé : ce sont les adresses de Dimdam's et de ses clients.
Ajoutécontent/hebergement/bases-de-donnees.md+39−0
@@ -0,0 +1,39 @@
1+---
2+title: Bases de données
3+description: MySQL, PostgreSQL, MongoDB et Redis gérés par Dimdam's.
4+order: 7
5+---
6+
7+Les bases de données Dimdam's tournent sur des serveurs dédiés, en France. Vous n'avez ni serveur à installer ni mise à jour à faire.
8+
9+## Se connecter
10+
11+Les identifiants (nom de la base, utilisateur, mot de passe) sont affichés sur la page du service. L'hôte est `db.dimdams.com`, avec le port habituel du moteur :
12+
13+| Moteur | Port |
14+| --- | --- |
15+| MySQL | `3306` |
16+| PostgreSQL | `5432` |
17+| MongoDB | `27017` |
18+| Redis | `6379` |
19+
20+Par exemple, en PostgreSQL :
21+
22+```bash
23+psql "postgresql://UTILISATEUR:MOT_DE_PASSE@db.dimdams.com:5432/NOM_DE_LA_BASE"
24+```
25+
26+## Pare-feu
27+
28+Le pare-feu de la base n'accepte que les adresses IP que vous autorisez. Ajoutez celles de vos serveurs de production, ou la vôtre pour administrer la base.
29+
30+## Utilisateurs
31+
32+Vous pouvez créer des utilisateurs supplémentaires, par exemple un accès en lecture seule pour un outil d'analyse.
33+
34+## Export et import
35+
36+- **Exporter** télécharge un fichier `.sql` de toute la base.
37+- **Importer** restaure ou migre une base depuis un fichier `.sql`, sans terminal.
38+
39+Pensez aussi aux [sauvegardes](/hebergement/sauvegardes/) régulières.
Ajoutécontent/hebergement/bots.md+26−0
@@ -0,0 +1,26 @@
1+---
2+title: Bots
3+description: Héberger un bot Discord ou tout programme qui tourne en continu.
4+order: 10
5+---
6+
7+Un service **Bot** fait tourner votre programme en continu : bot Discord, worker, tâche planifiée…
8+
9+## Langages
10+
11+Node.js, Python, Java, Go et Rust. Choisissez le langage et sa version lors de la [configuration](/hebergement/configurer/).
12+
13+## Mettre votre code en ligne
14+
15+- par l'[explorateur de fichiers](/hebergement/fichiers/) du tableau de bord ;
16+- ou par le [déploiement Git](/hebergement/deploiement-git/), pour une mise à jour automatique à chaque `git push`.
17+
18+Les variables d'environnement (jeton du bot, clés d'API…) se règlent dans les paramètres du service : ne les écrivez pas dans votre code.
19+
20+## Console
21+
22+La console affiche la sortie de votre programme en direct et permet de lui envoyer des commandes.
23+
24+## Être prévenu en cas d'arrêt
25+
26+Créez une [alerte](/api/alertes/) sur l'état du service, avec un webhook Discord : vous recevez un message dans votre salon dès que le bot s'arrête.
Ajoutécontent/hebergement/commander.md+20−0
@@ -0,0 +1,20 @@
1+---
2+title: Commander un service
3+description: Choisir un service dans la boutique, régler ses ressources et payer.
4+order: 1
5+---
6+
7+## Choisir le service
8+
9+Rendez-vous dans la [boutique]({{MAIN_SITE_URL}}/shop) et choisissez le type de service : hébergement web, bot, VPS, serveur de jeu, base de données… La liste complète est sur la page [Hébergement](/hebergement/).
10+
11+## Régler les ressources
12+
13+Ajustez la mémoire (RAM), le nombre de cœurs de processeur et l'espace disque. Le prix mensuel se met à jour au fur et à mesure. Ces ressources sont réservées à votre service : elles ne sont pas partagées avec les autres clients.
14+
15+> [!TIP]
16+> Commencez petit : vous pourrez augmenter les ressources plus tard depuis le tableau de bord, sans réinstaller le service. Seule la différence de prix pour les jours restants est facturée (voir [Facturation](/hebergement/facturation/)).
17+
18+## Payer
19+
20+Le paiement passe par Stripe (carte bancaire et les moyens proposés au moment du paiement). Une fois le paiement validé, le service apparaît dans votre tableau de bord, prêt à être [configuré](/hebergement/configurer/).
Ajoutécontent/hebergement/configurer.md+19−0
@@ -0,0 +1,19 @@
1+---
2+title: Configurer un service
3+description: La première configuration après la commande : nom, logiciel et version.
4+order: 2
5+---
6+
7+Un service qui vient d'être commandé apparaît dans le tableau de bord avec l'état « À configurer ». Cliquez sur **Configurer** pour lancer l'assistant.
8+
9+## L'assistant
10+
11+1. **Nom :** donnez un nom à votre service, pour le retrouver facilement (par exemple `api-production`). Il sert aussi à le désigner dans la [CLI](/api/cli/).
12+2. **Logiciel :** choisissez ce que le service fait tourner. Pour Minecraft, par exemple : Vanilla, Paper, Spigot, Forge, Fabric ou Purpur. Pour un site web : Apache, Nginx, PHP, Node.js ou Python. Pour un VPS : la distribution Linux.
13+3. **Version :** la version du logiciel (Minecraft 1.21, Node.js 22…).
14+4. **Accès :** pour un VPS, le mot de passe `root` et, si vous le souhaitez, votre clé SSH publique.
15+
16+L'installation prend en général moins d'une minute. Vous pouvez suivre son avancement dans la console.
17+
18+> [!NOTE]
19+> Pour changer de logiciel plus tard, une réinitialisation complète du service est possible dans ses réglages. Elle efface ses fichiers : faites une [sauvegarde](/hebergement/sauvegardes/) avant.
Ajoutécontent/hebergement/deploiement-git.md+28−0
@@ -0,0 +1,28 @@
1+---
2+title: Déploiement Git
3+description: Mettre à jour un site ou un bot automatiquement à chaque git push.
4+order: 9
5+---
6+
7+Reliez un site web ou un bot à un dépôt Git : à chaque `git push` sur la branche choisie, Dimdam's récupère le code, lance votre commande de construction et redémarre le service.
8+
9+Le dépôt peut être sur [Dimdam's Git](/git/), GitHub, GitLab ou n'importe quel serveur Git.
10+
11+## Mise en place
12+
13+Dans l'onglet **Git** du service :
14+
15+1. **Dépôt :** l'adresse HTTPS ou SSH du dépôt, et la branche à déployer (par défaut `main`).
16+2. **Clé de déploiement :** pour un dépôt privé, copiez la clé SSH publique affichée et ajoutez-la au dépôt en lecture seule (sur Dimdam's Git ou GitHub : **Paramètres › Clés de déploiement**).
17+3. **Webhook :** copiez l'adresse du webhook et son secret, et ajoutez-les dans les webhooks du dépôt (événement « push »). Sur Dimdam's Git, voir [Webhooks](/git/automatisation/#webhooks).
18+4. **Commande de construction** (facultative), lancée après chaque récupération du code, par exemple :
19+
20+```bash
21+npm ci && npm run build
22+```
23+
24+## Suivre les déploiements
25+
26+Chaque déploiement a son journal dans l'onglet **Git** : récupération du code, construction, redémarrage. En cas d'échec, le journal indique l'étape et l'erreur.
27+
28+Vous pouvez aussi lancer un déploiement à la main, depuis le tableau de bord, avec `dimdams deploy mon-service` (voir la [CLI](/api/cli/)) ou par l'[API](/api/deploiement-git/).
Ajoutécontent/hebergement/equipes.md+33−0
@@ -0,0 +1,33 @@
1+---
2+title: Équipes
3+description: Donner accès à vos services sans partager votre mot de passe.
4+order: 12
5+---
6+
7+Ne partagez jamais votre mot de passe : invitez vos collaborateurs dans une équipe.
8+
9+## Créer une équipe
10+
11+Créez une équipe dans votre espace client et invitez vos collaborateurs par e-mail. Chacun doit avoir son propre compte Dimdam's.
12+
13+## Rôles et permissions
14+
15+Chaque membre a un rôle, qui fixe ce qu'il peut faire sur les services de l'équipe :
16+
17+| Permission | Ce qu'elle permet |
18+| --- | --- |
19+| Lecture | Voir les services, leur état et leur consommation |
20+| Journaux | Lire les journaux et la console |
21+| Écriture | Modifier les fichiers et la configuration |
22+| Terminal | Utiliser la console et le terminal |
23+| Administration | Tout gérer, y compris les membres de l'équipe |
24+
25+Les membres d'une équipe ne voient jamais les secrets du propriétaire (mots de passe, jetons) dans la configuration des services partagés.
26+
27+## Double authentification obligatoire
28+
29+Une équipe peut exiger que tous ses membres aient activé la double authentification sur leur compte.
30+
31+## Journal d'activité
32+
33+Chaque action importante (démarrage, arrêt, sauvegarde, modification du pare-feu…) est enregistrée, avec son auteur. Voir aussi le [journal d'audit par l'API](/api/journal-audit/).
Ajoutécontent/hebergement/facturation.md+17−0
@@ -0,0 +1,17 @@
1+---
2+title: Facturation
3+description: Abonnement mensuel, changement de ressources et factures.
4+order: 13
5+---
6+
7+## Abonnement
8+
9+Chaque service est un abonnement mensuel, sans engagement. Le paiement passe par Stripe. Vos factures sont dans la page **Facturation** de votre espace client.
10+
11+## Changer les ressources
12+
13+Vous pouvez augmenter la mémoire, le processeur ou le disque d'un service à tout moment, depuis sa page. Vous ne payez que la différence de prix, pour les jours restants du mois en cours. Le nouveau prix s'applique ensuite chaque mois.
14+
15+## Options d'un VPS
16+
17+Les ports, sous-domaines et domaines personnalisés des VPS ont leurs propres quotas. Voir [VPS](/hebergement/vps/#ouvrir-un-port).
Ajoutécontent/hebergement/fichiers.md+28−0
@@ -0,0 +1,28 @@
1+---
2+title: Fichiers et accès
3+description: L'explorateur de fichiers du tableau de bord, et l'accès SSH / SFTP des VPS.
4+order: 4
5+---
6+
7+## L'explorateur de fichiers
8+
9+Tous les services ont un explorateur de fichiers dans le tableau de bord :
10+
11+- **éditeur de code** avec coloration, pour modifier un `.env`, un `server.properties` ou un script sans rien installer ;
12+- **envoi et téléchargement** de fichiers, et téléchargement d'un dossier entier en archive ;
13+- création, renommage et suppression de fichiers et de dossiers.
14+
15+## SSH et SFTP pour les VPS
16+
17+Les VPS sont accessibles en SSH par la passerelle de Dimdam's, sur le port `2222` :
18+
19+```bash
20+ssh root.<identifiant-du-vps>@ssh.dimdams.com -p 2222
21+```
22+
23+Par exemple : `ssh root.vps-1793740523964-d3bw0r@ssh.dimdams.com -p 2222`. L'identifiant du VPS est affiché sur sa page. Remplacez `root` par un autre utilisateur du VPS pour vous connecter avec lui.
24+
25+Les transferts de fichiers passent par la même adresse, avec `scp`, `sftp` ou un client graphique comme FileZilla ou WinSCP (protocole SFTP, port `2222`).
26+
27+> [!TIP]
28+> Ajoutez votre clé SSH publique dans l'onglet **Sécurité** du VPS : vous vous connecterez sans mot de passe. Plus d'informations dans [VPS](/hebergement/vps/).
Ajoutécontent/hebergement/gerer.md+33−0
@@ -0,0 +1,33 @@
1+---
2+title: Gérer un service
3+description: Démarrer, arrêter, suivre la console et la consommation de vos services.
4+order: 3
5+---
6+
7+Chaque service a sa page dans le tableau de bord, avec son état, ses commandes et sa consommation en direct.
8+
9+## Démarrer, arrêter, redémarrer
10+
11+- **Démarrer** lance le service.
12+- **Redémarrer** l'arrête proprement puis le relance : utile après un changement de configuration.
13+- **Arrêter** l'arrête proprement. Si le service ne répond plus, un bouton **Forcer l'arrêt** apparaît.
14+
15+Ces actions sont aussi disponibles par l'[API](/api/services/) et la [CLI](/api/cli/) (`dimdams restart mon-service`).
16+
17+## Console
18+
19+La console affiche ce que le service écrit (ses journaux) en direct. Pour un serveur de jeu ou un bot, vous pouvez aussi y taper des commandes.
20+
21+## Consommation
22+
23+Les graphiques montrent l'utilisation du processeur, de la mémoire et du réseau, sur les dernières heures, 7 jours ou 30 jours.
24+
25+- **Processeur :** s'il reste longtemps proche de 100 %, le service ralentit. Envisagez plus de cœurs.
26+- **Mémoire :** certaines applications (Java, par exemple) réservent de la mémoire dès le démarrage sans l'utiliser entièrement.
27+- **Réseau :** un pic inhabituel peut signaler une attaque ou une boucle dans votre code.
28+
29+Pour être prévenu automatiquement, créez des [alertes](/api/alertes/) : sur Discord, par e-mail ou dans le tableau de bord, quand une ressource dépasse un seuil ou quand le service s'arrête.
30+
31+## Fichiers
32+
33+Voir [Fichiers et accès](/hebergement/fichiers/).
Ajoutécontent/hebergement/index.md+27−0
@@ -0,0 +1,27 @@
1+---
2+title: Hébergement
3+description: Commander, configurer et gérer vos services sur Dimdam's.
4+---
5+
6+Dimdam's héberge vos projets sur ses propres serveurs, en France. Chaque service a ses ressources réservées (processeur, mémoire, disque), un tableau de bord pour le piloter, et une adresse en `dimdams.net`.
7+
8+## Les services proposés
9+
10+| Service | Pour quoi faire |
11+| --- | --- |
12+| **Hébergement web** | Sites et applications : Apache, Nginx, PHP, Node.js, Python |
13+| **Bots** | Bots Discord et autres programmes qui tournent en continu : Node.js, Python, Java, Go, Rust |
14+| **VPS** | Un serveur Linux complet avec accès `root` : Ubuntu, Debian, Fedora, Rocky Linux, AlmaLinux, CentOS Stream, Alpine, Arch Linux |
15+| **Serveurs de jeu** | Minecraft (Vanilla, Paper, Spigot, Forge, Fabric, Purpur), FiveM, Garry's Mod, Rust, Palworld, Counter-Strike 2, Valheim, ARK |
16+| **Bases de données** | MySQL, PostgreSQL, MongoDB et Redis |
17+| **Voix et audio** | TeamSpeak, Mumble, Lavalink |
18+| **Stockage S3** | Stockage d'objets compatible S3 |
19+| **Applications en un clic** | Des applications prêtes à l'emploi, installées pour vous |
20+
21+## Par où commencer
22+
23+1. [Commander un service](/hebergement/commander/) dans la boutique.
24+2. [Le configurer](/hebergement/configurer/) : nom, logiciel, version.
25+3. [Le gérer](/hebergement/gerer/) depuis le tableau de bord : démarrage, console, fichiers, consommation.
26+
27+Pour automatiser tout cela depuis vos scripts, voyez l'[API et la CLI](/api/).
Ajoutécontent/hebergement/sauvegardes.md+26−0
@@ -0,0 +1,26 @@
1+---
2+title: Sauvegardes
3+description: Sauvegarder un service à la demande ou automatiquement, et le restaurer.
4+order: 8
5+---
6+
7+Une sauvegarde contient tous les fichiers de votre service à un instant donné. Elles sont conservées sur l'infrastructure de Dimdam's, en France.
8+
9+## Sauvegarder
10+
11+- **À la demande :** bouton **Créer une sauvegarde** dans l'onglet **Sauvegardes** du service, ou `dimdams backup mon-service` avec la [CLI](/api/cli/).
12+- **Automatiquement :** activez la planification, chaque jour ou chaque semaine, à l'heure de votre choix. Vous choisissez aussi combien de sauvegardes garder : jusqu'à 30 quotidiennes et 12 hebdomadaires. Les plus anciennes sont supprimées au fur et à mesure.
13+
14+> [!TIP]
15+> Faites une sauvegarde manuelle avant toute opération risquée : mise à jour d'un plugin, changement de version, réinitialisation.
16+
17+## Restaurer
18+
19+Choisissez une sauvegarde dans la liste et cliquez sur **Restaurer**. Le service est arrêté, ses fichiers sont remplacés par ceux de la sauvegarde, puis il redémarre.
20+
21+> [!WARNING]
22+> La restauration remplace les fichiers actuels du service. Ce qui a été modifié depuis la sauvegarde est perdu : sauvegardez d'abord l'état actuel si vous pourriez en avoir besoin.
23+
24+## Par l'API
25+
26+Lister, créer et planifier les sauvegardes : voir [l'API des sauvegardes](/api/sauvegardes/).
Ajoutécontent/hebergement/serveurs-de-jeu.md+27−0
@@ -0,0 +1,27 @@
1+---
2+title: Serveurs de jeu
3+description: Minecraft, FiveM et les autres jeux : version, connexion des joueurs et console.
4+order: 11
5+---
6+
7+## Jeux disponibles
8+
9+| Jeu | Versions et variantes |
10+| --- | --- |
11+| Minecraft | Vanilla, Paper, Spigot, Forge, Fabric, Purpur |
12+| FiveM | ESX, QBCore, VORP, sans framework |
13+| Garry's Mod, Rust, Palworld, Counter-Strike 2, Valheim, ARK | Serveur dédié du jeu |
14+| TeamSpeak, Mumble | Serveurs vocaux |
15+
16+## Connexion des joueurs
17+
18+L'adresse du serveur est affichée sur sa page, avec son port : par exemple `mon-serveur.play.dimdams.net:25565`. Donnez-la à vos joueurs.
19+
20+## Gérer le serveur
21+
22+- **Console :** commandes du serveur (`op`, `whitelist`, `stop`…) et journaux en direct.
23+- **Fichiers :** `server.properties`, plugins, mods et mondes, depuis l'[explorateur](/hebergement/fichiers/).
24+- **Sauvegardes :** sauvegardez le monde avant une mise à jour ou l'ajout d'un plugin. Voir [Sauvegardes](/hebergement/sauvegardes/).
25+
26+> [!TIP]
27+> Si le serveur manque de mémoire (lenteurs, joueurs déconnectés, erreurs `OutOfMemoryError`), augmentez sa RAM depuis le tableau de bord : seule la différence de prix est facturée.
Ajoutécontent/hebergement/vps.md+45−0
@@ -0,0 +1,45 @@
1+---
2+title: VPS
3+description: Un serveur Linux complet avec accès root : connexion SSH, ports, IPv6 et domaines.
4+order: 5
5+---
6+
7+Un VPS Dimdam's est un serveur Linux complet : `systemd`, vos services (`cron`, `docker`, bases de données…), et un accès `root`. Il tourne sur nos serveurs en France, avec des ressources qui lui sont réservées.
8+
9+## Se connecter
10+
11+Par SSH, via la passerelle de Dimdam's :
12+
13+```bash
14+ssh root.<identifiant-du-vps>@ssh.dimdams.com -p 2222
15+```
16+
17+Le **terminal web** de la page du VPS donne aussi un shell complet dans le navigateur, sans client SSH.
18+
19+Pour vous connecter sans mot de passe, ajoutez votre clé publique (RSA ou Ed25519) dans l'onglet **Sécurité** du VPS : elle est ajoutée aux `authorized_keys` de `root`.
20+
21+## Réseau
22+
23+Chaque VPS a deux accès au réseau :
24+
25+- **Une adresse IPv6 publique dédiée :** tous les ports sont joignables directement, sans rien configurer. Elle est affichée dans l'onglet **Réseau**.
26+- **Une adresse IPv4 partagée :** pour les visiteurs sans IPv6, vous ouvrez les ports dont vous avez besoin, ou vous passez par un sous-domaine ou un domaine personnalisé.
27+
28+### Ouvrir un port
29+
30+Dans **Réseau › Ports ouverts & redirections**, cliquez sur **Ouvrir un port** et indiquez le port sur lequel votre application écoute dans le VPS (par exemple `4000`). Dimdam's lui attribue un port public : votre application est alors joignable sur `<identifiant-du-vps>.vps.dimdams.net:<port public>`.
31+
32+Le bouton **Tester** vérifie qu'un programme écoute bien sur ce port dans le VPS.
33+
34+> [!WARNING]
35+> Votre application doit écouter sur toutes les interfaces (`0.0.0.0` ou `::`), pas seulement sur `127.0.0.1` / `localhost` : sinon elle n'est pas joignable depuis l'extérieur du VPS.
36+
37+| | Inclus | Au-delà |
38+| --- | --- | --- |
39+| Ports ouverts | 3 | 0,50 € par mois et par port, jusqu'à 10 |
40+| Sous-domaines | 1 | 1,00 € par mois et par sous-domaine, jusqu'à 3 |
41+| Domaines personnalisés | Sans limite | Gratuits |
42+
43+### Sous-domaine et domaine personnalisé
44+
45+Pour une application web, liez plutôt un sous-domaine (`mon-app.vps.dimdams.net`) ou votre propre domaine à un port : le trafic arrive en HTTPS, avec un certificat automatique, sans numéro de port dans l'adresse. Voir [Adresses et domaines](/hebergement/adresses-et-domaines/).
Ajoutécontent/status/api.md+52−0
@@ -0,0 +1,52 @@
1+---
2+title: API publique
3+description: Lire l'état des services de Dimdam's depuis vos outils.
4+order: 1
5+---
6+
7+Les données de [Dimdam's Status]({{STATUS_URL}}) sont disponibles en JSON, sans authentification. Elles sont mises à jour toutes les 15 secondes environ : inutile d'interroger plus souvent.
8+
9+Les routes prennent le nom de la page de statut (`<page>`), celui qui apparaît dans son adresse. La page principale s'appelle `dimdams`.
10+
11+## Services et incidents
12+
13+`GET /api/status-page/<page>`
14+
15+La configuration de la page : ses groupes de services, les incidents en cours et les maintenances.
16+
17+```bash
18+curl "{{STATUS_URL}}/api/status-page/dimdams"
19+```
20+
21+## Vérifications et disponibilité
22+
23+`GET /api/status-page/heartbeat/<page>`
24+
25+Les dernières vérifications de chaque service (dans `heartbeatList`, `status` vaut `1` si le service répond, `0` sinon) et sa disponibilité sur 24 heures (`uptimeList`).
26+
27+```bash
28+curl "{{STATUS_URL}}/api/status-page/heartbeat/dimdams"
29+```
30+
31+Extrait de réponse :
32+
33+```json
34+{
35+ "heartbeatList": {
36+ "12": [
37+ { "status": 1, "time": "2026-11-04 09:41:07", "msg": "200 - OK", "ping": 48 }
38+ ]
39+ },
40+ "uptimeList": { "12_24": 0.9993 }
41+}
42+```
43+
44+## Historique des incidents
45+
46+`GET /api/status-page/<page>/incident-history`
47+
48+Les incidents passés, les plus récents en premier.
49+
50+```bash
51+curl "{{STATUS_URL}}/api/status-page/dimdams/incident-history"
52+```
Ajoutécontent/status/index.md+34−0
@@ -0,0 +1,34 @@
1+---
2+title: Dimdam's Status
3+description: Savoir en un coup d'œil si les services de Dimdam's fonctionnent.
4+---
5+
6+[Dimdam's Status]({{STATUS_URL}}) montre en temps réel l'état des services de Dimdam's : le site et le panel, l'hébergement, Dimdam's Git, l'API… Si quelque chose ne marche pas chez vous, commencez par là.
7+
8+## Lire la page
9+
10+Chaque service est vérifié automatiquement, toutes les minutes environ, depuis nos serveurs. Pour chacun, la page affiche :
11+
12+- **son état actuel :** opérationnel, perturbé ou en panne ;
13+- **une barre d'historique :** une case par vérification récente, verte si elle a réussi, rouge si elle a échoué ; survolez une case pour voir l'heure et le temps de réponse ;
14+- **sa disponibilité :** la part des vérifications réussies sur les dernières 24 heures et les derniers 30 jours ;
15+- **son adresse et son certificat** quand le service est un site : un clic ouvre le site, et la pastille indique dans combien de jours son certificat HTTPS expire.
16+
17+## Couleurs de disponibilité
18+
19+| Disponibilité | Couleur |
20+| --- | --- |
21+| 99,9 % et plus | Vert |
22+| De 98 % à 99,9 % | Orange |
23+| Moins de 98 % | Rouge |
24+
25+## Incidents et maintenances
26+
27+Quand un problème est en cours, un bandeau en haut de la page l'explique, et il est mis à jour jusqu'à sa résolution. Les maintenances prévues y sont annoncées à l'avance. L'historique des incidents passés est en bas de la page, avec un calendrier.
28+
29+> [!NOTE]
30+> La plupart des courtes coupures viennent du déploiement d'une mise à jour : le service redémarre, puis revient en quelques secondes. Elles sont signalées comme telles dans l'historique.
31+
32+## Être prévenu
33+
34+Pour vos propres services, configurez des [alertes](/api/alertes/) dans le panel : vous êtes prévenu par e-mail ou sur Discord quand l'un d'eux s'arrête ou consomme trop.
Ajouténext.config.ts+13−0
@@ -0,0 +1,13 @@
1+import type { NextConfig } from "next";
2+
3+// Static site: `next build` writes out/ (one folder and index.html per page), served by nginx.
4+// Every setting comes from the .env, read at build time (src/lib/env.ts checks them).
5+
6+const nextConfig: NextConfig = {
7+ output: "export",
8+ trailingSlash: true,
9+ poweredByHeader: false,
10+ images: { unoptimized: true },
11+};
12+
13+export default nextConfig;
Ajoutépackage-lock.json+1648−0
@@ -0,0 +1,1648 @@
1+{
2+ "name": "dimdams-docs",
3+ "version": "1.0.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "dimdams-docs",
9+ "version": "1.0.0",
10+ "dependencies": {
11+ "marked": "18.1.0",
12+ "next": "16.4.4",
13+ "react": "19.2.3",
14+ "react-dom": "19.2.3",
15+ "shiki": "4.6.0"
16+ },
17+ "devDependencies": {
18+ "@types/node": "^20.19.27",
19+ "@types/react": "^19",
20+ "@types/react-dom": "^19",
21+ "typescript": "^5"
22+ }
23+ },
24+ "node_modules/@emnapi/runtime": {
25+ "version": "1.11.3",
26+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
27+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
28+ "license": "MIT",
29+ "optional": true,
30+ "dependencies": {
31+ "tslib": "^2.4.0"
32+ }
33+ },
34+ "node_modules/@img/colour": {
35+ "version": "1.1.0",
36+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
37+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
38+ "license": "MIT",
39+ "optional": true,
40+ "engines": {
41+ "node": ">=18"
42+ }
43+ },
44+ "node_modules/@img/sharp-darwin-arm64": {
45+ "version": "0.35.5",
46+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
47+ "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
48+ "cpu": [
49+ "arm64"
50+ ],
51+ "license": "Apache-2.0",
52+ "optional": true,
53+ "os": [
54+ "darwin"
55+ ],
56+ "engines": {
57+ "node": ">=20.9.0"
58+ },
59+ "funding": {
60+ "url": "https://opencollective.com/libvips"
61+ },
62+ "optionalDependencies": {
63+ "@img/sharp-libvips-darwin-arm64": "1.3.4"
64+ }
65+ },
66+ "node_modules/@img/sharp-darwin-x64": {
67+ "version": "0.35.5",
68+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
69+ "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
70+ "cpu": [
71+ "x64"
72+ ],
73+ "license": "Apache-2.0",
74+ "optional": true,
75+ "os": [
76+ "darwin"
77+ ],
78+ "engines": {
79+ "node": ">=20.9.0"
80+ },
81+ "funding": {
82+ "url": "https://opencollective.com/libvips"
83+ },
84+ "optionalDependencies": {
85+ "@img/sharp-libvips-darwin-x64": "1.3.4"
86+ }
87+ },
88+ "node_modules/@img/sharp-freebsd-wasm32": {
89+ "version": "0.35.5",
90+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
91+ "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
92+ "license": "Apache-2.0",
93+ "optional": true,
94+ "os": [
95+ "freebsd"
96+ ],
97+ "dependencies": {
98+ "@img/sharp-wasm32": "0.35.5"
99+ },
100+ "engines": {
101+ "node": ">=20.9.0"
102+ },
103+ "funding": {
104+ "url": "https://opencollective.com/libvips"
105+ }
106+ },
107+ "node_modules/@img/sharp-libvips-darwin-arm64": {
108+ "version": "1.3.4",
109+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
110+ "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
111+ "cpu": [
112+ "arm64"
113+ ],
114+ "license": "LGPL-3.0-or-later",
115+ "optional": true,
116+ "os": [
117+ "darwin"
118+ ],
119+ "funding": {
120+ "url": "https://opencollective.com/libvips"
121+ }
122+ },
123+ "node_modules/@img/sharp-libvips-darwin-x64": {
124+ "version": "1.3.4",
125+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
126+ "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
127+ "cpu": [
128+ "x64"
129+ ],
130+ "license": "LGPL-3.0-or-later",
131+ "optional": true,
132+ "os": [
133+ "darwin"
134+ ],
135+ "funding": {
136+ "url": "https://opencollective.com/libvips"
137+ }
138+ },
139+ "node_modules/@img/sharp-libvips-linux-arm": {
140+ "version": "1.3.4",
141+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
142+ "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
143+ "cpu": [
144+ "arm"
145+ ],
146+ "libc": [
147+ "glibc"
148+ ],
149+ "license": "LGPL-3.0-or-later",
150+ "optional": true,
151+ "os": [
152+ "linux"
153+ ],
154+ "funding": {
155+ "url": "https://opencollective.com/libvips"
156+ }
157+ },
158+ "node_modules/@img/sharp-libvips-linux-arm64": {
159+ "version": "1.3.4",
160+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
161+ "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
162+ "cpu": [
163+ "arm64"
164+ ],
165+ "libc": [
166+ "glibc"
167+ ],
168+ "license": "LGPL-3.0-or-later",
169+ "optional": true,
170+ "os": [
171+ "linux"
172+ ],
173+ "funding": {
174+ "url": "https://opencollective.com/libvips"
175+ }
176+ },
177+ "node_modules/@img/sharp-libvips-linux-ppc64": {
178+ "version": "1.3.4",
179+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
180+ "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
181+ "cpu": [
182+ "ppc64"
183+ ],
184+ "libc": [
185+ "glibc"
186+ ],
187+ "license": "LGPL-3.0-or-later",
188+ "optional": true,
189+ "os": [
190+ "linux"
191+ ],
192+ "funding": {
193+ "url": "https://opencollective.com/libvips"
194+ }
195+ },
196+ "node_modules/@img/sharp-libvips-linux-riscv64": {
197+ "version": "1.3.4",
198+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
199+ "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
200+ "cpu": [
201+ "riscv64"
202+ ],
203+ "libc": [
204+ "glibc"
205+ ],
206+ "license": "LGPL-3.0-or-later",
207+ "optional": true,
208+ "os": [
209+ "linux"
210+ ],
211+ "funding": {
212+ "url": "https://opencollective.com/libvips"
213+ }
214+ },
215+ "node_modules/@img/sharp-libvips-linux-s390x": {
216+ "version": "1.3.4",
217+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
218+ "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
219+ "cpu": [
220+ "s390x"
221+ ],
222+ "libc": [
223+ "glibc"
224+ ],
225+ "license": "LGPL-3.0-or-later",
226+ "optional": true,
227+ "os": [
228+ "linux"
229+ ],
230+ "funding": {
231+ "url": "https://opencollective.com/libvips"
232+ }
233+ },
234+ "node_modules/@img/sharp-libvips-linux-x64": {
235+ "version": "1.3.4",
236+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
237+ "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
238+ "cpu": [
239+ "x64"
240+ ],
241+ "libc": [
242+ "glibc"
243+ ],
244+ "license": "LGPL-3.0-or-later",
245+ "optional": true,
246+ "os": [
247+ "linux"
248+ ],
249+ "funding": {
250+ "url": "https://opencollective.com/libvips"
251+ }
252+ },
253+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
254+ "version": "1.3.4",
255+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
256+ "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
257+ "cpu": [
258+ "arm64"
259+ ],
260+ "libc": [
261+ "musl"
262+ ],
263+ "license": "LGPL-3.0-or-later",
264+ "optional": true,
265+ "os": [
266+ "linux"
267+ ],
268+ "funding": {
269+ "url": "https://opencollective.com/libvips"
270+ }
271+ },
272+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
273+ "version": "1.3.4",
274+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
275+ "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
276+ "cpu": [
277+ "x64"
278+ ],
279+ "libc": [
280+ "musl"
281+ ],
282+ "license": "LGPL-3.0-or-later",
283+ "optional": true,
284+ "os": [
285+ "linux"
286+ ],
287+ "funding": {
288+ "url": "https://opencollective.com/libvips"
289+ }
290+ },
291+ "node_modules/@img/sharp-linux-arm": {
292+ "version": "0.35.5",
293+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
294+ "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
295+ "cpu": [
296+ "arm"
297+ ],
298+ "libc": [
299+ "glibc"
300+ ],
301+ "license": "Apache-2.0",
302+ "optional": true,
303+ "os": [
304+ "linux"
305+ ],
306+ "engines": {
307+ "node": ">=20.9.0"
308+ },
309+ "funding": {
310+ "url": "https://opencollective.com/libvips"
311+ },
312+ "optionalDependencies": {
313+ "@img/sharp-libvips-linux-arm": "1.3.4"
314+ }
315+ },
316+ "node_modules/@img/sharp-linux-arm64": {
317+ "version": "0.35.5",
318+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
319+ "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
320+ "cpu": [
321+ "arm64"
322+ ],
323+ "libc": [
324+ "glibc"
325+ ],
326+ "license": "Apache-2.0",
327+ "optional": true,
328+ "os": [
329+ "linux"
330+ ],
331+ "engines": {
332+ "node": ">=20.9.0"
333+ },
334+ "funding": {
335+ "url": "https://opencollective.com/libvips"
336+ },
337+ "optionalDependencies": {
338+ "@img/sharp-libvips-linux-arm64": "1.3.4"
339+ }
340+ },
341+ "node_modules/@img/sharp-linux-ppc64": {
342+ "version": "0.35.5",
343+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
344+ "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
345+ "cpu": [
346+ "ppc64"
347+ ],
348+ "libc": [
349+ "glibc"
350+ ],
351+ "license": "Apache-2.0",
352+ "optional": true,
353+ "os": [
354+ "linux"
355+ ],
356+ "engines": {
357+ "node": ">=20.9.0"
358+ },
359+ "funding": {
360+ "url": "https://opencollective.com/libvips"
361+ },
362+ "optionalDependencies": {
363+ "@img/sharp-libvips-linux-ppc64": "1.3.4"
364+ }
365+ },
366+ "node_modules/@img/sharp-linux-riscv64": {
367+ "version": "0.35.5",
368+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
369+ "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
370+ "cpu": [
371+ "riscv64"
372+ ],
373+ "libc": [
374+ "glibc"
375+ ],
376+ "license": "Apache-2.0",
377+ "optional": true,
378+ "os": [
379+ "linux"
380+ ],
381+ "engines": {
382+ "node": ">=20.9.0"
383+ },
384+ "funding": {
385+ "url": "https://opencollective.com/libvips"
386+ },
387+ "optionalDependencies": {
388+ "@img/sharp-libvips-linux-riscv64": "1.3.4"
389+ }
390+ },
391+ "node_modules/@img/sharp-linux-s390x": {
392+ "version": "0.35.5",
393+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
394+ "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
395+ "cpu": [
396+ "s390x"
397+ ],
398+ "libc": [
399+ "glibc"
400+ ],
401+ "license": "Apache-2.0",
402+ "optional": true,
403+ "os": [
404+ "linux"
405+ ],
406+ "engines": {
407+ "node": ">=20.9.0"
408+ },
409+ "funding": {
410+ "url": "https://opencollective.com/libvips"
411+ },
412+ "optionalDependencies": {
413+ "@img/sharp-libvips-linux-s390x": "1.3.4"
414+ }
415+ },
416+ "node_modules/@img/sharp-linux-x64": {
417+ "version": "0.35.5",
418+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
419+ "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
420+ "cpu": [
421+ "x64"
422+ ],
423+ "libc": [
424+ "glibc"
425+ ],
426+ "license": "Apache-2.0",
427+ "optional": true,
428+ "os": [
429+ "linux"
430+ ],
431+ "engines": {
432+ "node": ">=20.9.0"
433+ },
434+ "funding": {
435+ "url": "https://opencollective.com/libvips"
436+ },
437+ "optionalDependencies": {
438+ "@img/sharp-libvips-linux-x64": "1.3.4"
439+ }
440+ },
441+ "node_modules/@img/sharp-linuxmusl-arm64": {
442+ "version": "0.35.5",
443+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
444+ "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
445+ "cpu": [
446+ "arm64"
447+ ],
448+ "libc": [
449+ "musl"
450+ ],
451+ "license": "Apache-2.0",
452+ "optional": true,
453+ "os": [
454+ "linux"
455+ ],
456+ "engines": {
457+ "node": ">=20.9.0"
458+ },
459+ "funding": {
460+ "url": "https://opencollective.com/libvips"
461+ },
462+ "optionalDependencies": {
463+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
464+ }
465+ },
466+ "node_modules/@img/sharp-linuxmusl-x64": {
467+ "version": "0.35.5",
468+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
469+ "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
470+ "cpu": [
471+ "x64"
472+ ],
473+ "libc": [
474+ "musl"
475+ ],
476+ "license": "Apache-2.0",
477+ "optional": true,
478+ "os": [
479+ "linux"
480+ ],
481+ "engines": {
482+ "node": ">=20.9.0"
483+ },
484+ "funding": {
485+ "url": "https://opencollective.com/libvips"
486+ },
487+ "optionalDependencies": {
488+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4"
489+ }
490+ },
491+ "node_modules/@img/sharp-wasm32": {
492+ "version": "0.35.5",
493+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
494+ "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
495+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
496+ "optional": true,
497+ "dependencies": {
498+ "@emnapi/runtime": "^1.11.3"
499+ },
500+ "engines": {
501+ "node": ">=20.9.0"
502+ },
503+ "funding": {
504+ "url": "https://opencollective.com/libvips"
505+ }
506+ },
507+ "node_modules/@img/sharp-webcontainers-wasm32": {
508+ "version": "0.35.5",
509+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
510+ "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
511+ "cpu": [
512+ "wasm32"
513+ ],
514+ "license": "Apache-2.0",
515+ "optional": true,
516+ "dependencies": {
517+ "@img/sharp-wasm32": "0.35.5"
518+ },
519+ "engines": {
520+ "node": ">=20.9.0"
521+ },
522+ "funding": {
523+ "url": "https://opencollective.com/libvips"
524+ }
525+ },
526+ "node_modules/@img/sharp-win32-arm64": {
527+ "version": "0.35.5",
528+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
529+ "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
530+ "cpu": [
531+ "arm64"
532+ ],
533+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
534+ "optional": true,
535+ "os": [
536+ "win32"
537+ ],
538+ "engines": {
539+ "node": ">=20.9.0"
540+ },
541+ "funding": {
542+ "url": "https://opencollective.com/libvips"
543+ }
544+ },
545+ "node_modules/@img/sharp-win32-ia32": {
546+ "version": "0.35.5",
547+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
548+ "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
549+ "cpu": [
550+ "ia32"
551+ ],
552+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
553+ "optional": true,
554+ "os": [
555+ "win32"
556+ ],
557+ "engines": {
558+ "node": "^20.9.0"
559+ },
560+ "funding": {
561+ "url": "https://opencollective.com/libvips"
562+ }
563+ },
564+ "node_modules/@img/sharp-win32-x64": {
565+ "version": "0.35.5",
566+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
567+ "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
568+ "cpu": [
569+ "x64"
570+ ],
571+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
572+ "optional": true,
573+ "os": [
574+ "win32"
575+ ],
576+ "engines": {
577+ "node": ">=20.9.0"
578+ },
579+ "funding": {
580+ "url": "https://opencollective.com/libvips"
581+ }
582+ },
583+ "node_modules/@next/env": {
584+ "version": "16.4.4",
585+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.4.tgz",
586+ "integrity": "sha512-6/3ZuELR0tPLgNigvv8d/LAmfLIo1IpNfvrI/AHCAl9w9D0DNVGQGvo+OLPzAWR+WMW3vVyO7HaW0kfL97huBg==",
587+ "license": "MIT"
588+ },
589+ "node_modules/@next/swc-darwin-arm64": {
590+ "version": "16.4.4",
591+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.4.tgz",
592+ "integrity": "sha512-1KlZ5IJ7zcLBZvqG1q7nuRWm2LAAN4C3iBqqoBCRRl1UEv0A4k6Rek+x3j23UiWf8Gz+VsN+M/LylAWkxpm2mg==",
593+ "cpu": [
594+ "arm64"
595+ ],
596+ "license": "MIT",
597+ "optional": true,
598+ "os": [
599+ "darwin"
600+ ],
601+ "engines": {
602+ "node": ">= 10"
603+ }
604+ },
605+ "node_modules/@next/swc-darwin-x64": {
606+ "version": "16.4.4",
607+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.4.tgz",
608+ "integrity": "sha512-Tb7efFdhbS/ph74RS7wob84gXGM58qxoO/WrkiMsTmy/JZktKOKBeiM45W0E/b8JfBzZJdexQqSmvedBwDK0PA==",
609+ "cpu": [
610+ "x64"
611+ ],
612+ "license": "MIT",
613+ "optional": true,
614+ "os": [
615+ "darwin"
616+ ],
617+ "engines": {
618+ "node": ">= 10"
619+ }
620+ },
621+ "node_modules/@next/swc-linux-arm64-gnu": {
622+ "version": "16.4.4",
623+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.4.tgz",
624+ "integrity": "sha512-wojSfLnH8RRImhtbcn3Dlx9JICh6aQHgDA33XDr+EPua2tt7Q/yjDn0FTAbqfNlLrBmOASAGhLh6NvBk9L3gzw==",
625+ "cpu": [
626+ "arm64"
627+ ],
628+ "libc": [
629+ "glibc"
630+ ],
631+ "license": "MIT",
632+ "optional": true,
633+ "os": [
634+ "linux"
635+ ],
636+ "engines": {
637+ "node": ">= 10"
638+ }
639+ },
640+ "node_modules/@next/swc-linux-arm64-musl": {
641+ "version": "16.4.4",
642+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.4.tgz",
643+ "integrity": "sha512-zgxpGOZaR+2O6dOk5ZAUkKgH92ZtqNg3ohzyg2HmWc2N+AZH4dEpTwdpB0e5MR9YRVdVDUkEM5g6c3TWWn8g1g==",
644+ "cpu": [
645+ "arm64"
646+ ],
647+ "libc": [
648+ "musl"
649+ ],
650+ "license": "MIT",
651+ "optional": true,
652+ "os": [
653+ "linux"
654+ ],
655+ "engines": {
656+ "node": ">= 10"
657+ }
658+ },
659+ "node_modules/@next/swc-linux-x64-gnu": {
660+ "version": "16.4.4",
661+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.4.tgz",
662+ "integrity": "sha512-fg3Wr030Y3BDkCXi6HGsPz0BUPTGZJS8TZSrCO0tS3O/KBsCyx4eP6aeaizCpGmcUKhtb4l7JoMc88QO2qBl2g==",
663+ "cpu": [
664+ "x64"
665+ ],
666+ "libc": [
667+ "glibc"
668+ ],
669+ "license": "MIT",
670+ "optional": true,
671+ "os": [
672+ "linux"
673+ ],
674+ "engines": {
675+ "node": ">= 10"
676+ }
677+ },
678+ "node_modules/@next/swc-linux-x64-musl": {
679+ "version": "16.4.4",
680+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.4.tgz",
681+ "integrity": "sha512-40cr71PSdCV9zU2g0B1XG27af10tYU+9g8F+FtF3qSguQg0jSZDoxn0NrAvgFQ0cy9s0ZBA/00P9g/XL1O43xg==",
682+ "cpu": [
683+ "x64"
684+ ],
685+ "libc": [
686+ "musl"
687+ ],
688+ "license": "MIT",
689+ "optional": true,
690+ "os": [
691+ "linux"
692+ ],
693+ "engines": {
694+ "node": ">= 10"
695+ }
696+ },
697+ "node_modules/@next/swc-win32-arm64-msvc": {
698+ "version": "16.4.4",
699+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.4.tgz",
700+ "integrity": "sha512-vFMiEbRZ+LYNeswslf4LuGuoPHE8JbtqV0GnmzbpnGNMs0uWaR2q98hu8M/UWGycNJERBXIUss+BGd3PIJ8VZg==",
701+ "cpu": [
702+ "arm64"
703+ ],
704+ "license": "MIT",
705+ "optional": true,
706+ "os": [
707+ "win32"
708+ ],
709+ "engines": {
710+ "node": ">= 10"
711+ }
712+ },
713+ "node_modules/@next/swc-win32-x64-msvc": {
714+ "version": "16.4.4",
715+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.4.tgz",
716+ "integrity": "sha512-jy2P4V8BDZ+CFCzYABHYRZ1CmyFhfmYe22i8KA4uEafdYPrUjJ3Ot+lx265mLWLHiZRIfBzCn3V1F17m0vzl9Q==",
717+ "cpu": [
718+ "x64"
719+ ],
720+ "license": "MIT",
721+ "optional": true,
722+ "os": [
723+ "win32"
724+ ],
725+ "engines": {
726+ "node": ">= 10"
727+ }
728+ },
729+ "node_modules/@shikijs/core": {
730+ "version": "4.6.0",
731+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.6.0.tgz",
732+ "integrity": "sha512-bfd4OKgvoyXYiYmMRuleA656P4V7M/Du9lcTl38Ngm6KszMj7bq4J1IGZwSYR4c95Tv5UQALtYNWUwaI9UT3xw==",
733+ "license": "MIT",
734+ "dependencies": {
735+ "@shikijs/primitive": "4.6.0",
736+ "@shikijs/types": "4.6.0",
737+ "@shikijs/vscode-textmate": "^10.1.0",
738+ "@types/hast": "^3.0.5",
739+ "hast-util-to-html": "^9.0.6"
740+ },
741+ "engines": {
742+ "node": ">=20"
743+ }
744+ },
745+ "node_modules/@shikijs/engine-javascript": {
746+ "version": "4.6.0",
747+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.6.0.tgz",
748+ "integrity": "sha512-K0oqxFoL+kEMdz89mHLFO15Tsbv3kFZ6WR1cXZKMvLSc+oqaM00A6pyga1qSeXB22dP0tTiSApRcGBaKWsEccg==",
749+ "license": "MIT",
750+ "dependencies": {
751+ "@shikijs/types": "4.6.0",
752+ "@shikijs/vscode-textmate": "^10.1.0",
753+ "oniguruma-to-es": "^4.3.6"
754+ },
755+ "engines": {
756+ "node": ">=20"
757+ }
758+ },
759+ "node_modules/@shikijs/engine-oniguruma": {
760+ "version": "4.6.0",
761+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.6.0.tgz",
762+ "integrity": "sha512-aggd6Q7SuJOXNnsUbfG2XesxBJKBMAw/PDTUo9PyRjMaaJJ2dNrI+q0C+25ARXAK9yLHAlwxc9oRT2u68YKZ7w==",
763+ "license": "MIT",
764+ "dependencies": {
765+ "@shikijs/types": "4.6.0",
766+ "@shikijs/vscode-textmate": "^10.1.0"
767+ },
768+ "engines": {
769+ "node": ">=20"
770+ }
771+ },
772+ "node_modules/@shikijs/langs": {
773+ "version": "4.6.0",
774+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.6.0.tgz",
775+ "integrity": "sha512-1yMEX3oCVV54LUq/ovfpRkDQoYDkcvjSWFFa/YLUDMiSoRzMi2uxLJYbv41ahT8Lu9lFMaa7hecd3Q39trqUyg==",
776+ "license": "MIT",
777+ "dependencies": {
778+ "@shikijs/types": "4.6.0"
779+ },
780+ "engines": {
781+ "node": ">=20"
782+ }
783+ },
784+ "node_modules/@shikijs/primitive": {
785+ "version": "4.6.0",
786+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.6.0.tgz",
787+ "integrity": "sha512-c54+TQEo9ToqgfL/AU8FpjnV09+GA9Oa7M5ROkkXteZfp5XzpAJGXjjvZQFQioJOwm/w/6Z3lIzQstAkl8coag==",
788+ "license": "MIT",
789+ "dependencies": {
790+ "@shikijs/types": "4.6.0",
791+ "@shikijs/vscode-textmate": "^10.1.0",
792+ "@types/hast": "^3.0.5"
793+ },
794+ "engines": {
795+ "node": ">=20"
796+ }
797+ },
798+ "node_modules/@shikijs/themes": {
799+ "version": "4.6.0",
800+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.6.0.tgz",
801+ "integrity": "sha512-qHAVAs80RiGD+IRuH9G+JPDOEc9A/kHsng4EzCUCwB9bLzW6Z0y9WETh2YyCcADJBmfrIaJ1Jp7KBNP6FHkgtQ==",
802+ "license": "MIT",
803+ "dependencies": {
804+ "@shikijs/types": "4.6.0"
805+ },
806+ "engines": {
807+ "node": ">=20"
808+ }
809+ },
810+ "node_modules/@shikijs/types": {
811+ "version": "4.6.0",
812+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.6.0.tgz",
813+ "integrity": "sha512-+ENG1adsKt/X7cRYw1Nl7BEpBmPOjmQ7bR/eGmHuMlT8gtOFf3KEi5l31DJYyAWhW6Y6bjP1plU7FmaxWGENFw==",
814+ "license": "MIT",
815+ "dependencies": {
816+ "@shikijs/vscode-textmate": "^10.1.0",
817+ "@types/hast": "^3.0.5"
818+ },
819+ "engines": {
820+ "node": ">=20"
821+ }
822+ },
823+ "node_modules/@shikijs/vscode-textmate": {
824+ "version": "10.1.0",
825+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz",
826+ "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==",
827+ "license": "MIT"
828+ },
829+ "node_modules/@swc/helpers": {
830+ "version": "0.5.23",
831+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
832+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
833+ "license": "Apache-2.0",
834+ "dependencies": {
835+ "tslib": "^2.8.0"
836+ }
837+ },
838+ "node_modules/@types/hast": {
839+ "version": "3.0.5",
840+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
841+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
842+ "license": "MIT",
843+ "dependencies": {
844+ "@types/unist": "*"
845+ }
846+ },
847+ "node_modules/@types/mdast": {
848+ "version": "4.0.4",
849+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
850+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
851+ "license": "MIT",
852+ "dependencies": {
853+ "@types/unist": "*"
854+ }
855+ },
856+ "node_modules/@types/node": {
857+ "version": "20.19.43",
858+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
859+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
860+ "dev": true,
861+ "license": "MIT",
862+ "dependencies": {
863+ "undici-types": "~6.21.0"
864+ }
865+ },
866+ "node_modules/@types/react": {
867+ "version": "19.3.0",
868+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
869+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
870+ "dev": true,
871+ "license": "MIT",
872+ "dependencies": {
873+ "csstype": "^3.2.2"
874+ }
875+ },
876+ "node_modules/@types/react-dom": {
877+ "version": "19.3.0",
878+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
879+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
880+ "dev": true,
881+ "license": "MIT",
882+ "peerDependencies": {
883+ "@types/react": "^19.3.0"
884+ }
885+ },
886+ "node_modules/@types/unist": {
887+ "version": "3.0.3",
888+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
889+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
890+ "license": "MIT"
891+ },
892+ "node_modules/@ungap/structured-clone": {
893+ "version": "1.4.0",
894+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
895+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
896+ "license": "ISC"
897+ },
898+ "node_modules/baseline-browser-mapping": {
899+ "version": "2.11.35",
900+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz",
901+ "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==",
902+ "license": "Apache-2.0",
903+ "bin": {
904+ "baseline-browser-mapping": "dist/cli.cjs"
905+ },
906+ "engines": {
907+ "node": ">=6.0.0"
908+ }
909+ },
910+ "node_modules/caniuse-lite": {
911+ "version": "1.0.30001820",
912+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz",
913+ "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==",
914+ "funding": [
915+ {
916+ "type": "opencollective",
917+ "url": "https://opencollective.com/browserslist"
918+ },
919+ {
920+ "type": "tidelift",
921+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
922+ },
923+ {
924+ "type": "github",
925+ "url": "https://github.com/sponsors/ai"
926+ }
927+ ],
928+ "license": "CC-BY-4.0"
929+ },
930+ "node_modules/ccount": {
931+ "version": "2.0.1",
932+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
933+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
934+ "license": "MIT",
935+ "funding": {
936+ "type": "github",
937+ "url": "https://github.com/sponsors/wooorm"
938+ }
939+ },
940+ "node_modules/character-entities-html4": {
941+ "version": "2.1.0",
942+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
943+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
944+ "license": "MIT",
945+ "funding": {
946+ "type": "github",
947+ "url": "https://github.com/sponsors/wooorm"
948+ }
949+ },
950+ "node_modules/character-entities-legacy": {
951+ "version": "3.0.0",
952+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
953+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
954+ "license": "MIT",
955+ "funding": {
956+ "type": "github",
957+ "url": "https://github.com/sponsors/wooorm"
958+ }
959+ },
960+ "node_modules/client-only": {
961+ "version": "0.0.1",
962+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
963+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
964+ "license": "MIT"
965+ },
966+ "node_modules/comma-separated-tokens": {
967+ "version": "2.0.3",
968+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
969+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
970+ "license": "MIT",
971+ "funding": {
972+ "type": "github",
973+ "url": "https://github.com/sponsors/wooorm"
974+ }
975+ },
976+ "node_modules/csstype": {
977+ "version": "3.2.3",
978+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
979+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
980+ "dev": true,
981+ "license": "MIT"
982+ },
983+ "node_modules/dequal": {
984+ "version": "2.0.3",
985+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
986+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
987+ "license": "MIT",
988+ "engines": {
989+ "node": ">=6"
990+ }
991+ },
992+ "node_modules/detect-libc": {
993+ "version": "2.1.2",
994+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
995+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
996+ "license": "Apache-2.0",
997+ "optional": true,
998+ "engines": {
999+ "node": ">=8"
1000+ }
1001+ },
1002+ "node_modules/devlop": {
1003+ "version": "1.1.0",
1004+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
1005+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
1006+ "license": "MIT",
1007+ "dependencies": {
1008+ "dequal": "^2.0.0"
1009+ },
1010+ "funding": {
1011+ "type": "github",
1012+ "url": "https://github.com/sponsors/wooorm"
1013+ }
1014+ },
1015+ "node_modules/hast-util-to-html": {
1016+ "version": "9.0.6",
1017+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.6.tgz",
1018+ "integrity": "sha512-1SZUxCN3TtJAE7e2BQN2MBHsoZ1gnmi9GoCP5Di59CSWptIezSAW09yDZyVbanb78V9eCwrKqdQsjtfVTxYHWA==",
1019+ "license": "MIT",
1020+ "dependencies": {
1021+ "@types/hast": "^3.0.0",
1022+ "@types/unist": "^3.0.0",
1023+ "ccount": "^2.0.0",
1024+ "comma-separated-tokens": "^2.0.0",
1025+ "hast-util-whitespace": "^3.0.0",
1026+ "html-void-elements": "^3.0.0",
1027+ "mdast-util-to-hast": "^13.0.0",
1028+ "property-information": "^7.0.0",
1029+ "space-separated-tokens": "^2.0.0",
1030+ "stringify-entities": "^4.0.0",
1031+ "zwitch": "^2.0.4"
1032+ },
1033+ "funding": {
1034+ "type": "opencollective",
1035+ "url": "https://opencollective.com/unified"
1036+ }
1037+ },
1038+ "node_modules/hast-util-whitespace": {
1039+ "version": "3.0.0",
1040+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
1041+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
1042+ "license": "MIT",
1043+ "dependencies": {
1044+ "@types/hast": "^3.0.0"
1045+ },
1046+ "funding": {
1047+ "type": "opencollective",
1048+ "url": "https://opencollective.com/unified"
1049+ }
1050+ },
1051+ "node_modules/html-void-elements": {
1052+ "version": "3.0.0",
1053+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
1054+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
1055+ "license": "MIT",
1056+ "funding": {
1057+ "type": "github",
1058+ "url": "https://github.com/sponsors/wooorm"
1059+ }
1060+ },
1061+ "node_modules/marked": {
1062+ "version": "18.1.0",
1063+ "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.0.tgz",
1064+ "integrity": "sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==",
1065+ "license": "MIT",
1066+ "bin": {
1067+ "marked": "bin/marked.js"
1068+ },
1069+ "engines": {
1070+ "node": ">= 20"
1071+ }
1072+ },
1073+ "node_modules/mdast-util-to-hast": {
1074+ "version": "13.2.1",
1075+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
1076+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
1077+ "license": "MIT",
1078+ "dependencies": {
1079+ "@types/hast": "^3.0.0",
1080+ "@types/mdast": "^4.0.0",
1081+ "@ungap/structured-clone": "^1.0.0",
1082+ "devlop": "^1.0.0",
1083+ "micromark-util-sanitize-uri": "^2.0.0",
1084+ "trim-lines": "^3.0.0",
1085+ "unist-util-position": "^5.0.0",
1086+ "unist-util-visit": "^5.0.0",
1087+ "vfile": "^6.0.0"
1088+ },
1089+ "funding": {
1090+ "type": "opencollective",
1091+ "url": "https://opencollective.com/unified"
1092+ }
1093+ },
1094+ "node_modules/micromark-util-character": {
1095+ "version": "2.1.1",
1096+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
1097+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
1098+ "funding": [
1099+ {
1100+ "type": "GitHub Sponsors",
1101+ "url": "https://github.com/sponsors/unifiedjs"
1102+ },
1103+ {
1104+ "type": "OpenCollective",
1105+ "url": "https://opencollective.com/unified"
1106+ }
1107+ ],
1108+ "license": "MIT",
1109+ "dependencies": {
1110+ "micromark-util-symbol": "^2.0.0",
1111+ "micromark-util-types": "^2.0.0"
1112+ }
1113+ },
1114+ "node_modules/micromark-util-encode": {
1115+ "version": "2.0.1",
1116+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
1117+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
1118+ "funding": [
1119+ {
1120+ "type": "GitHub Sponsors",
1121+ "url": "https://github.com/sponsors/unifiedjs"
1122+ },
1123+ {
1124+ "type": "OpenCollective",
1125+ "url": "https://opencollective.com/unified"
1126+ }
1127+ ],
1128+ "license": "MIT"
1129+ },
1130+ "node_modules/micromark-util-sanitize-uri": {
1131+ "version": "2.0.1",
1132+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
1133+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
1134+ "funding": [
1135+ {
1136+ "type": "GitHub Sponsors",
1137+ "url": "https://github.com/sponsors/unifiedjs"
1138+ },
1139+ {
1140+ "type": "OpenCollective",
1141+ "url": "https://opencollective.com/unified"
1142+ }
1143+ ],
1144+ "license": "MIT",
1145+ "dependencies": {
1146+ "micromark-util-character": "^2.0.0",
1147+ "micromark-util-encode": "^2.0.0",
1148+ "micromark-util-symbol": "^2.0.0"
1149+ }
1150+ },
1151+ "node_modules/micromark-util-symbol": {
1152+ "version": "2.0.1",
1153+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
1154+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
1155+ "funding": [
1156+ {
1157+ "type": "GitHub Sponsors",
1158+ "url": "https://github.com/sponsors/unifiedjs"
1159+ },
1160+ {
1161+ "type": "OpenCollective",
1162+ "url": "https://opencollective.com/unified"
1163+ }
1164+ ],
1165+ "license": "MIT"
1166+ },
1167+ "node_modules/micromark-util-types": {
1168+ "version": "2.0.3",
1169+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz",
1170+ "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==",
1171+ "funding": [
1172+ {
1173+ "type": "GitHub Sponsors",
1174+ "url": "https://github.com/sponsors/unifiedjs"
1175+ },
1176+ {
1177+ "type": "OpenCollective",
1178+ "url": "https://opencollective.com/unified"
1179+ }
1180+ ],
1181+ "license": "MIT"
1182+ },
1183+ "node_modules/nanoid": {
1184+ "version": "3.3.20",
1185+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
1186+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
1187+ "funding": [
1188+ {
1189+ "type": "github",
1190+ "url": "https://github.com/sponsors/ai"
1191+ }
1192+ ],
1193+ "license": "MIT",
1194+ "bin": {
1195+ "nanoid": "bin/nanoid.cjs"
1196+ },
1197+ "engines": {
1198+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1199+ }
1200+ },
1201+ "node_modules/next": {
1202+ "version": "16.4.4",
1203+ "resolved": "https://registry.npmjs.org/next/-/next-16.4.4.tgz",
1204+ "integrity": "sha512-Hu4pOw2x0RQ0TQabNWbpsNg/3+SZf+rLQwvfhBLnptv98GS4dcIVMlfEyn9jb2/ZJDSahhnUKEs0YrfarjGX3A==",
1205+ "license": "MIT",
1206+ "dependencies": {
1207+ "@next/env": "16.4.4",
1208+ "@swc/helpers": "0.5.23",
1209+ "baseline-browser-mapping": "^2.9.19",
1210+ "caniuse-lite": "^1.0.30001579",
1211+ "postcss": "8.5.23",
1212+ "styled-jsx": "5.1.6"
1213+ },
1214+ "bin": {
1215+ "next": "dist/bin/next"
1216+ },
1217+ "engines": {
1218+ "node": ">=20.9.0"
1219+ },
1220+ "optionalDependencies": {
1221+ "@next/swc-darwin-arm64": "16.4.4",
1222+ "@next/swc-darwin-x64": "16.4.4",
1223+ "@next/swc-linux-arm64-gnu": "16.4.4",
1224+ "@next/swc-linux-arm64-musl": "16.4.4",
1225+ "@next/swc-linux-x64-gnu": "16.4.4",
1226+ "@next/swc-linux-x64-musl": "16.4.4",
1227+ "@next/swc-win32-arm64-msvc": "16.4.4",
1228+ "@next/swc-win32-x64-msvc": "16.4.4",
1229+ "sharp": "^0.35.5"
1230+ },
1231+ "peerDependencies": {
1232+ "@opentelemetry/api": "^1.1.0",
1233+ "@playwright/test": "^1.51.1",
1234+ "babel-plugin-react-compiler": "*",
1235+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1236+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1237+ "sass": "^1.3.0"
1238+ },
1239+ "peerDependenciesMeta": {
1240+ "@opentelemetry/api": {
1241+ "optional": true
1242+ },
1243+ "@playwright/test": {
1244+ "optional": true
1245+ },
1246+ "babel-plugin-react-compiler": {
1247+ "optional": true
1248+ },
1249+ "sass": {
1250+ "optional": true
1251+ }
1252+ }
1253+ },
1254+ "node_modules/oniguruma-parser": {
1255+ "version": "0.12.2",
1256+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
1257+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
1258+ "license": "MIT"
1259+ },
1260+ "node_modules/oniguruma-to-es": {
1261+ "version": "4.3.6",
1262+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
1263+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
1264+ "license": "MIT",
1265+ "dependencies": {
1266+ "oniguruma-parser": "^0.12.2",
1267+ "regex": "^6.1.0",
1268+ "regex-recursion": "^6.0.2"
1269+ }
1270+ },
1271+ "node_modules/picocolors": {
1272+ "version": "1.1.1",
1273+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1274+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1275+ "license": "ISC"
1276+ },
1277+ "node_modules/postcss": {
1278+ "version": "8.5.23",
1279+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
1280+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
1281+ "funding": [
1282+ {
1283+ "type": "opencollective",
1284+ "url": "https://opencollective.com/postcss/"
1285+ },
1286+ {
1287+ "type": "tidelift",
1288+ "url": "https://tidelift.com/funding/github/npm/postcss"
1289+ },
1290+ {
1291+ "type": "github",
1292+ "url": "https://github.com/sponsors/ai"
1293+ }
1294+ ],
1295+ "license": "MIT",
1296+ "dependencies": {
1297+ "nanoid": "^3.3.16",
1298+ "picocolors": "^1.1.1",
1299+ "source-map-js": "^1.2.1"
1300+ },
1301+ "engines": {
1302+ "node": "^10 || ^12 || >=14"
1303+ }
1304+ },
1305+ "node_modules/property-information": {
1306+ "version": "7.2.0",
1307+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
1308+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
1309+ "license": "MIT",
1310+ "funding": {
1311+ "type": "github",
1312+ "url": "https://github.com/sponsors/wooorm"
1313+ }
1314+ },
1315+ "node_modules/react": {
1316+ "version": "19.2.3",
1317+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
1318+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
1319+ "license": "MIT",
1320+ "engines": {
1321+ "node": ">=0.10.0"
1322+ }
1323+ },
1324+ "node_modules/react-dom": {
1325+ "version": "19.2.3",
1326+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
1327+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
1328+ "license": "MIT",
1329+ "dependencies": {
1330+ "scheduler": "^0.27.0"
1331+ },
1332+ "peerDependencies": {
1333+ "react": "^19.2.3"
1334+ }
1335+ },
1336+ "node_modules/regex": {
1337+ "version": "6.1.0",
1338+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
1339+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
1340+ "license": "MIT",
1341+ "dependencies": {
1342+ "regex-utilities": "^2.3.0"
1343+ }
1344+ },
1345+ "node_modules/regex-recursion": {
1346+ "version": "6.0.2",
1347+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
1348+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
1349+ "license": "MIT",
1350+ "dependencies": {
1351+ "regex-utilities": "^2.3.0"
1352+ }
1353+ },
1354+ "node_modules/regex-utilities": {
1355+ "version": "2.3.0",
1356+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
1357+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
1358+ "license": "MIT"
1359+ },
1360+ "node_modules/scheduler": {
1361+ "version": "0.27.0",
1362+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1363+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1364+ "license": "MIT"
1365+ },
1366+ "node_modules/semver": {
1367+ "version": "7.8.6",
1368+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
1369+ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
1370+ "license": "ISC",
1371+ "optional": true,
1372+ "bin": {
1373+ "semver": "bin/semver.js"
1374+ },
1375+ "engines": {
1376+ "node": ">=10"
1377+ }
1378+ },
1379+ "node_modules/sharp": {
1380+ "version": "0.35.5",
1381+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
1382+ "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
1383+ "license": "Apache-2.0",
1384+ "optional": true,
1385+ "dependencies": {
1386+ "@img/colour": "^1.1.0",
1387+ "detect-libc": "^2.1.2",
1388+ "semver": "^7.8.5"
1389+ },
1390+ "engines": {
1391+ "node": ">=20.9.0"
1392+ },
1393+ "funding": {
1394+ "url": "https://opencollective.com/libvips"
1395+ },
1396+ "optionalDependencies": {
1397+ "@img/sharp-darwin-arm64": "0.35.5",
1398+ "@img/sharp-darwin-x64": "0.35.5",
1399+ "@img/sharp-freebsd-wasm32": "0.35.5",
1400+ "@img/sharp-libvips-darwin-arm64": "1.3.4",
1401+ "@img/sharp-libvips-darwin-x64": "1.3.4",
1402+ "@img/sharp-libvips-linux-arm": "1.3.4",
1403+ "@img/sharp-libvips-linux-arm64": "1.3.4",
1404+ "@img/sharp-libvips-linux-ppc64": "1.3.4",
1405+ "@img/sharp-libvips-linux-riscv64": "1.3.4",
1406+ "@img/sharp-libvips-linux-s390x": "1.3.4",
1407+ "@img/sharp-libvips-linux-x64": "1.3.4",
1408+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
1409+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4",
1410+ "@img/sharp-linux-arm": "0.35.5",
1411+ "@img/sharp-linux-arm64": "0.35.5",
1412+ "@img/sharp-linux-ppc64": "0.35.5",
1413+ "@img/sharp-linux-riscv64": "0.35.5",
1414+ "@img/sharp-linux-s390x": "0.35.5",
1415+ "@img/sharp-linux-x64": "0.35.5",
1416+ "@img/sharp-linuxmusl-arm64": "0.35.5",
1417+ "@img/sharp-linuxmusl-x64": "0.35.5",
1418+ "@img/sharp-webcontainers-wasm32": "0.35.5",
1419+ "@img/sharp-win32-arm64": "0.35.5",
1420+ "@img/sharp-win32-ia32": "0.35.5",
1421+ "@img/sharp-win32-x64": "0.35.5"
1422+ },
1423+ "peerDependenciesMeta": {
1424+ "@types/node": {
1425+ "optional": true
1426+ }
1427+ }
1428+ },
1429+ "node_modules/shiki": {
1430+ "version": "4.6.0",
1431+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.6.0.tgz",
1432+ "integrity": "sha512-imjLsVHgPuWhvpU3P+zOekvMYqnel7QmKsFC4Sn0fCuAvaaalBsxwKEsrU8Ko5BdFnZA6Go/8a0RzahS634UgA==",
1433+ "license": "MIT",
1434+ "dependencies": {
1435+ "@shikijs/core": "4.6.0",
1436+ "@shikijs/engine-javascript": "4.6.0",
1437+ "@shikijs/engine-oniguruma": "4.6.0",
1438+ "@shikijs/langs": "4.6.0",
1439+ "@shikijs/themes": "4.6.0",
1440+ "@shikijs/types": "4.6.0",
1441+ "@shikijs/vscode-textmate": "^10.1.0",
1442+ "@types/hast": "^3.0.5"
1443+ },
1444+ "engines": {
1445+ "node": ">=20"
1446+ }
1447+ },
1448+ "node_modules/source-map-js": {
1449+ "version": "1.2.2",
1450+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
1451+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
1452+ "license": "BSD-3-Clause",
1453+ "engines": {
1454+ "node": ">=0.10.0"
1455+ }
1456+ },
1457+ "node_modules/space-separated-tokens": {
1458+ "version": "2.0.2",
1459+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
1460+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
1461+ "license": "MIT",
1462+ "funding": {
1463+ "type": "github",
1464+ "url": "https://github.com/sponsors/wooorm"
1465+ }
1466+ },
1467+ "node_modules/stringify-entities": {
1468+ "version": "4.0.4",
1469+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
1470+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
1471+ "license": "MIT",
1472+ "dependencies": {
1473+ "character-entities-html4": "^2.0.0",
1474+ "character-entities-legacy": "^3.0.0"
1475+ },
1476+ "funding": {
1477+ "type": "github",
1478+ "url": "https://github.com/sponsors/wooorm"
1479+ }
1480+ },
1481+ "node_modules/styled-jsx": {
1482+ "version": "5.1.6",
1483+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
1484+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
1485+ "license": "MIT",
1486+ "dependencies": {
1487+ "client-only": "0.0.1"
1488+ },
1489+ "engines": {
1490+ "node": ">= 12.0.0"
1491+ },
1492+ "peerDependencies": {
1493+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
1494+ },
1495+ "peerDependenciesMeta": {
1496+ "@babel/core": {
1497+ "optional": true
1498+ },
1499+ "babel-plugin-macros": {

Différences tronquées : fichier trop long.

Ajoutépackage.json+25−0
@@ -0,0 +1,25 @@
1+{
2+ "name": "dimdams-docs",
3+ "version": "1.0.0",
4+ "private": true,
5+ "description": "Documentation de Dimdam's (docs.dimdams.com) : hébergement, API et CLI, Dimdam's Git, Dimdam's Status.",
6+ "scripts": {
7+ "dev": "next dev -p 3010",
8+ "build": "next build",
9+ "check": "node scripts/check-content.mjs",
10+ "deploy": "bash scripts/deploy.sh"
11+ },
12+ "dependencies": {
13+ "marked": "18.1.0",
14+ "next": "16.4.4",
15+ "react": "19.2.3",
16+ "react-dom": "19.2.3",
17+ "shiki": "4.6.0"
18+ },
19+ "devDependencies": {
20+ "@types/node": "^20.19.27",
21+ "@types/react": "^19",
22+ "@types/react-dom": "^19",
23+ "typescript": "^5"
24+ }
25+}
Binairepublic/icon.webp+0−0

Fichier binaire : différences non affichées.

Binairepublic/logo-mark.png+0−0

Fichier binaire : différences non affichées.

Ajoutéscripts/check-content.mjs+102−0
@@ -0,0 +1,102 @@
1+import fs from "node:fs";
2+import path from "node:path";
3+
4+const ROOT = path.resolve(import.meta.dirname, "..");
5+const CONTENT_DIR = path.join(ROOT, "content");
6+const ALLOWED_VARS = new Set(["PUBLIC_URL", "MAIN_SITE_URL", "GIT_URL", "STATUS_URL"]);
7+const SECTIONS = ["hebergement", "api", "git", "status"];
8+
9+let errors = 0;
10+let warnings = 0;
11+let totalPages = 0;
12+
13+const pagesBySection = new Map();
14+
15+for (const section of SECTIONS) {
16+ const dir = path.join(CONTENT_DIR, section);
17+ if (!fs.existsSync(dir)) {
18+ console.error(`❌ Dossier manquant : ${section}`);
19+ errors++;
20+ continue;
21+ }
22+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
23+ const slugs = new Set(files.map((f) => f.replace(/\.md$/, "")));
24+ pagesBySection.set(section, slugs);
25+
26+ for (const file of files) {
27+ totalPages++;
28+ const fullPath = path.join(dir, file);
29+ const content = fs.readFileSync(fullPath, "utf8").replace(/\r\n/g, "\n");
30+
31+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
32+ if (!fmMatch) {
33+ console.error(`❌ En-tête manquant : ${section}/${file}`);
34+ errors++;
35+ continue;
36+ }
37+
38+ const meta = {};
39+ for (const line of fmMatch[1].split("\n")) {
40+ const kv = line.match(/^([a-zA-Z]+):\s*(.*)$/);
41+ if (kv) meta[kv[1]] = kv[2].trim().replace(/^"(.*)"$/, "$1");
42+ }
43+
44+ if (!meta.title) {
45+ console.error(`❌ Titre manquant : ${section}/${file}`);
46+ errors++;
47+ }
48+ if (!meta.description) {
49+ console.warn(`⚠️ Description manquante : ${section}/${file}`);
50+ warnings++;
51+ }
52+
53+ // Check variable substitutions
54+ const varMatches = content.matchAll(/\{\{([A-Z_]+)\}\}/g);
55+ for (const match of varMatches) {
56+ if (!ALLOWED_VARS.has(match[1])) {
57+ console.error(`❌ Variable inconnue {{${match[1]}}} dans ${section}/${file}`);
58+ errors++;
59+ }
60+ }
61+ }
62+}
63+
64+// Check internal links
65+for (const section of SECTIONS) {
66+ const dir = path.join(CONTENT_DIR, section);
67+ if (!fs.existsSync(dir)) continue;
68+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
69+
70+ for (const file of files) {
71+ const fullPath = path.join(dir, file);
72+ const content = fs.readFileSync(fullPath, "utf8");
73+
74+ // Matches [text](/section/slug/) or (/section/)
75+ const linkMatches = content.matchAll(/\]\((\/[a-z0-9_-]+\/[a-z0-9_#-]*)\)/g);
76+ for (const match of linkMatches) {
77+ const rawHref = match[1].split("#")[0]; // remove anchor
78+ const parts = rawHref.split("/").filter(Boolean);
79+ if (parts.length >= 1) {
80+ const targetSection = parts[0];
81+ const targetSlug = parts[1] || "index";
82+ const sectionSlugs = pagesBySection.get(targetSection);
83+ if (!sectionSlugs) {
84+ console.error(`❌ Lien vers section inconnue : ${rawHref} dans ${section}/${file}`);
85+ errors++;
86+ } else if (!sectionSlugs.has(targetSlug)) {
87+ console.error(`❌ Lien brisé : ${rawHref} dans ${section}/${file} (page ${targetSlug}.md introuvable)`);
88+ errors++;
89+ }
90+ }
91+ }
92+ }
93+}
94+
95+console.log(`\nVérification terminée : ${totalPages} pages analysées.`);
96+console.log(`Erreurs : ${errors}, Avertissements : ${warnings}`);
97+
98+if (errors > 0) {
99+ process.exit(1);
100+} else {
101+ console.log("✅ Contenu valide !");
102+}
Ajoutéscripts/deploy.sh+33−0
@@ -0,0 +1,33 @@
1+#!/usr/bin/env bash
2+# Déploie le code committé (HEAD) de Dimdam's Docs sur server-003 :
3+# envoie les fichiers, installe, compile le site statique (out/), sans PM2 car servi directement par Nginx.
4+# bash scripts/deploy.sh (depuis Git Bash sur le PC)
5+set -euo pipefail
6+
7+repo="$(cd "$(dirname "$0")/.." && pwd)"
8+cd "$repo"
9+
10+if [ "$(node -p "require('./package.json').name" 2>/dev/null)" != "dimdams-docs" ]; then
11+ echo "Ce script doit rester dans le dépôt dimdams-docs (dossier actuel : $repo)." >&2
12+ exit 1
13+fi
14+if [ -n "$(git status --porcelain)" ]; then
15+ echo "Attention : des modifications ne sont pas committées, seul le dernier commit est envoyé." >&2
16+fi
17+
18+server="${DEPLOY_SERVER:-root@192.168.1.131}"
19+app="/home/benladamm/dimdams-docs"
20+
21+echo "Déploiement de $(git log --oneline -1) vers $server:$app"
22+git archive HEAD | ssh "$server" "set -e
23+ mkdir -p $app
24+ cd $app
25+ tar -xf -
26+ if [ ! -f .env ]; then
27+ cp .env.example .env
28+ fi
29+ npm ci --no-audit --no-fund > /tmp/dd-npm.log 2>&1 || { tail -25 /tmp/dd-npm.log; exit 1; }
30+ npm run build > /tmp/dd-build.log 2>&1 || { tail -25 /tmp/dd-build.log; exit 1; }
31+ chown -R benladamm:benladamm $app
32+ echo 'Compilation statique : OK (out/)'"
33+echo "Terminé : https://docs.dimdams.com"
Ajoutésrc/app/[section]/[slug]/page.tsx+24−0
@@ -0,0 +1,24 @@
1+import type { Metadata } from "next";
2+import { notFound } from "next/navigation";
3+import { DocPage } from "@/components/DocPage";
4+import { getPage, listPages } from "@/lib/content";
5+import { SECTIONS } from "@/lib/sections";
6+
7+type Params = { params: Promise<{ section: string; slug: string }> };
8+
9+export const dynamicParams = false;
10+export const generateStaticParams = () =>
11+ SECTIONS.flatMap((s) => listPages(s.id).filter((p) => p.slug !== "index").map((p) => ({ section: s.id, slug: p.slug })));
12+
13+export async function generateMetadata({ params }: Params): Promise<Metadata> {
14+ const { section, slug } = await params;
15+ const page = await getPage(section, slug);
16+ return page ? { title: page.title, description: page.description || undefined, alternates: { canonical: page.href } } : {};
17+}
18+
19+export default async function ContentPage({ params }: Params) {
20+ const { section, slug } = await params;
21+ const page = await getPage(section, slug);
22+ if (!page) notFound();
23+ return <DocPage page={page} pages={listPages(section)} />;
24+}
Ajoutésrc/app/[section]/page.tsx+23−0
@@ -0,0 +1,23 @@
1+import type { Metadata } from "next";
2+import { notFound } from "next/navigation";
3+import { DocPage } from "@/components/DocPage";
4+import { getPage, listPages } from "@/lib/content";
5+import { SECTIONS, sectionById } from "@/lib/sections";
6+
7+type Params = { params: Promise<{ section: string }> };
8+
9+export const dynamicParams = false;
10+export const generateStaticParams = () => SECTIONS.map((s) => ({ section: s.id }));
11+
12+export async function generateMetadata({ params }: Params): Promise<Metadata> {
13+ const section = sectionById((await params).section);
14+ return section ? { title: section.title, description: section.description, alternates: { canonical: `/${section.id}/` } } : {};
15+}
16+
17+/** Home of a documentation: its index.md. */
18+export default async function SectionPage({ params }: Params) {
19+ const { section } = await params;
20+ const page = await getPage(section, "index");
21+ if (!page) notFound();
22+ return <DocPage page={page} pages={listPages(section)} />;
23+}
Ajoutésrc/app/globals.css+191−0
@@ -0,0 +1,191 @@
1+/* Dimdam's Docs: Dimdam's colors, dark and light theme (data-theme on <html>, otherwise the system's). */
2+
3+:root {
4+ --canvas: #09090b; --surface: #141417; --surface-2: #0f0f12; --line: rgba(255, 255, 255, 0.08); --line-2: rgba(255, 255, 255, 0.14);
5+ --text: #d9dce1; --muted: #9ca3af; --faint: #6b7280; --strong: #fff;
6+ --accent: #6b2030; --accent-hover: #561a26; --accent-light: #d0566e; --accent-soft: #f3b4c1;
7+ --note: #38bdf8; --note-soft: rgba(56, 189, 248, 0.1); --tip: #22c55e; --tip-soft: rgba(34, 197, 94, 0.1);
8+ --warn: #f59e0b; --warn-soft: rgba(245, 158, 11, 0.1); --danger: #f43f5e; --danger-soft: rgba(244, 63, 94, 0.1);
9+ --code-bg: #0d0d10; --inline-code: rgba(255, 255, 255, 0.07);
10+ color-scheme: dark;
11+}
12+:root[data-theme="light"] {
13+ --canvas: #f7f7f8; --surface: #fff; --surface-2: #f1f1f3; --line: rgba(0, 0, 0, 0.08); --line-2: rgba(0, 0, 0, 0.14);
14+ --text: #2b2f36; --muted: #4b5563; --faint: #8b93a1; --strong: #111827;
15+ --accent-light: #a3283f; --accent-soft: #6b2030;
16+ --note: #0284c7; --note-soft: rgba(2, 132, 199, 0.08); --tip: #15803d; --tip-soft: rgba(21, 128, 61, 0.08);
17+ --warn: #b45309; --warn-soft: rgba(217, 119, 6, 0.1); --danger: #be123c; --danger-soft: rgba(225, 29, 72, 0.08);
18+ --code-bg: #f6f6f8; --inline-code: rgba(0, 0, 0, 0.06);
19+ color-scheme: light;
20+}
21+@media (prefers-color-scheme: light) {
22+ :root:not([data-theme]) {
23+ --canvas: #f7f7f8; --surface: #fff; --surface-2: #f1f1f3; --line: rgba(0, 0, 0, 0.08); --line-2: rgba(0, 0, 0, 0.14);
24+ --text: #2b2f36; --muted: #4b5563; --faint: #8b93a1; --strong: #111827;
25+ --accent-light: #a3283f; --accent-soft: #6b2030;
26+ --note: #0284c7; --note-soft: rgba(2, 132, 199, 0.08); --tip: #15803d; --tip-soft: rgba(21, 128, 61, 0.08);
27+ --warn: #b45309; --warn-soft: rgba(217, 119, 6, 0.1); --danger: #be123c; --danger-soft: rgba(225, 29, 72, 0.08);
28+ --code-bg: #f6f6f8; --inline-code: rgba(0, 0, 0, 0.06);
29+ color-scheme: light;
30+ }
31+}
32+
33+* { box-sizing: border-box; }
34+html { scroll-padding-top: 88px; }
35+body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; background: var(--canvas); color: var(--text); font-family: var(--font-inter), system-ui, sans-serif; font-size: 16px; line-height: 1.65; -webkit-font-smoothing: antialiased; }
36+a { color: var(--accent-light); }
37+h1, h2, h3, h4 { font-family: var(--font-outfit), system-ui, sans-serif; color: var(--strong); line-height: 1.25; }
38+code, kbd, pre { font-family: var(--font-mono), ui-monospace, monospace; }
39+:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; border-radius: 4px; }
40+
41+/* Buttons */
42+.btn { font: inherit; font-size: 13px; font-weight: 600; padding: 8px 14px; border-radius: 10px; border: 1px solid var(--line-2); background: var(--surface); color: var(--text); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
43+.btn:hover { border-color: var(--faint); }
44+.btn.primary { background: var(--accent); border-color: #c2415a; color: #fff; }
45+.btn.primary:hover { background: var(--accent-hover); }
46+.btn.icon { padding: 8px; }
47+
48+/* Header */
49+.header { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--canvas) 85%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
50+.header-in { max-width: 1360px; margin: 0 auto; padding: 0 20px; height: 64px; display: flex; align-items: center; gap: 20px; }
51+.brand { display: flex; align-items: center; gap: 10px; font-family: var(--font-outfit), sans-serif; font-weight: 800; font-size: 18px; color: var(--strong); text-decoration: none; flex: none; }
52+.brand img { width: 36px; height: 36px; object-fit: contain; }
53+.tag { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: 999px; border: 1px solid var(--line-2); color: var(--accent-soft); }
54+.tabs { display: flex; gap: 2px; font-size: 14px; }
55+.tabs a { padding: 7px 12px; border-radius: 10px; color: var(--muted); text-decoration: none; white-space: nowrap; }
56+.tabs a:hover, .tabs a[aria-current="page"] { color: var(--strong); background: var(--surface); }
57+.tabs a[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--accent-light); }
58+.tabs-mobile { display: none; }
59+.header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
60+.search-button { font: inherit; font-size: 13px; display: inline-flex; align-items: center; gap: 8px; padding: 7px 10px; min-width: 210px; border-radius: 10px; border: 1px solid var(--line-2); background: var(--surface-2); color: var(--faint); cursor: pointer; }
61+.search-button:hover { border-color: var(--faint); color: var(--muted); }
62+.search-button kbd { margin-left: auto; }
63+kbd { font-size: 11px; padding: 1px 6px; border-radius: 6px; border: 1px solid var(--line-2); background: var(--surface); color: var(--muted); }
64+
65+/* Search */
66+.search-overlay { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, 0.55); backdrop-filter: blur(3px); display: flex; justify-content: center; padding: 10vh 16px 16px; }
67+.search-box { width: 100%; max-width: 640px; max-height: 75vh; display: flex; flex-direction: column; border-radius: 16px; border: 1px solid var(--line-2); background: var(--surface); box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45); overflow: hidden; }
68+.search-box input { font: inherit; font-size: 16px; padding: 16px 18px; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: var(--strong); outline: none; }
69+.search-results { list-style: none; margin: 0; padding: 6px; overflow-y: auto; }
70+.search-results button { width: 100%; text-align: left; font: inherit; display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-radius: 10px; border: 0; background: transparent; color: var(--text); cursor: pointer; }
71+.search-results button.active { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent-light); }
72+.search-where { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--faint); }
73+.search-title { font-weight: 600; color: var(--strong); }
74+.search-heading { font-weight: 500; color: var(--muted); }
75+.search-snippet { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76+.search-empty { padding: 14px 12px; color: var(--muted); font-size: 14px; }
77+.search-help { margin: 0; padding: 10px 16px; border-top: 1px solid var(--line); font-size: 12px; color: var(--faint); }
78+
79+/* Home */
80+.home { max-width: 1160px; margin: 0 auto; padding: 56px 20px 72px; width: 100%; flex: 1; }
81+.hero { text-align: center; margin-bottom: 44px; }
82+.hero h1 { font-size: clamp(30px, 5vw, 44px); margin: 0 0 12px; letter-spacing: -0.02em; }
83+.hero p { max-width: 640px; margin: 0 auto; color: var(--muted); font-size: 17px; }
84+.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; }
85+.card { display: flex; flex-direction: column; padding: 22px; border-radius: 16px; border: 1px solid var(--line); background: var(--surface); text-decoration: none; color: var(--text); transition: border-color 0.15s, transform 0.15s; }
86+.card:hover { border-color: color-mix(in srgb, var(--accent-light) 45%, transparent); transform: translateY(-2px); }
87+.card h2 { margin: 0 0 6px; font-size: 19px; }
88+.card p { margin: 0 0 14px; color: var(--muted); font-size: 14px; }
89+.card ul { margin: 0 0 14px; padding-left: 18px; font-size: 14px; color: var(--muted); }
90+.card-more { margin-top: auto; font-size: 13px; font-weight: 600; color: var(--accent-light); }
91+
92+/* Documentation page */
93+.doc { max-width: 1360px; margin: 0 auto; width: 100%; flex: 1; display: grid; grid-template-columns: 250px minmax(0, 1fr) 220px; gap: 40px; padding: 0 20px; }
94+.sidebar, .toc { position: sticky; top: 64px; align-self: start; max-height: calc(100vh - 64px); overflow-y: auto; padding: 28px 0; }
95+.sidebar-title, .toc-title { margin: 0 0 10px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--faint); }
96+.sidebar ul, .toc ul, .sidebar-mobile ul { list-style: none; margin: 0; padding: 0; }
97+.sidebar a, .sidebar-mobile a { display: block; padding: 6px 10px; border-radius: 8px; font-size: 14px; color: var(--muted); text-decoration: none; }
98+.sidebar a:hover, .sidebar-mobile a:hover { color: var(--strong); background: var(--surface); }
99+.sidebar a[aria-current="page"], .sidebar-mobile a[aria-current="page"] { color: var(--strong); background: var(--surface); font-weight: 600; box-shadow: inset 2px 0 0 var(--accent-light); }
100+.toc a { display: block; padding: 4px 0; font-size: 13px; color: var(--muted); text-decoration: none; }
101+.toc a:hover { color: var(--strong); }
102+.toc li.sub a { padding-left: 12px; font-size: 12.5px; }
103+.sidebar-mobile { display: none; }
104+.article { min-width: 0; max-width: 780px; padding: 32px 0 64px; }
105+.breadcrumb { margin: 0 0 8px; font-size: 13px; color: var(--faint); }
106+.breadcrumb a { color: var(--muted); text-decoration: none; }
107+.breadcrumb a:hover { color: var(--accent-light); }
108+.article > h1 { font-size: clamp(28px, 4vw, 36px); margin: 0 0 10px; letter-spacing: -0.015em; }
109+.lead { margin: 0 0 28px; font-size: 18px; color: var(--muted); }
110+
111+/* Content */
112+.prose h2 { font-size: 23px; margin: 44px 0 12px; padding-top: 16px; border-top: 1px solid var(--line); }
113+.prose h3 { font-size: 18px; margin: 30px 0 8px; }
114+.prose h4 { font-size: 16px; margin: 22px 0 6px; }
115+.prose h2, .prose h3, .prose h4 { position: relative; }
116+.prose .anchor { position: absolute; left: -22px; color: var(--faint); text-decoration: none; opacity: 0; font-weight: 500; }
117+.prose h2:hover .anchor, .prose h3:hover .anchor, .prose h4:hover .anchor { opacity: 1; }
118+.prose p, .prose ul, .prose ol { margin: 0 0 16px; }
119+.prose li { margin: 4px 0; }
120+.prose li > ul, .prose li > ol { margin: 4px 0; }
121+.prose strong { color: var(--strong); }
122+.prose a { text-underline-offset: 3px; }
123+.prose :not(pre) > code { font-size: 0.88em; padding: 2px 6px; border-radius: 6px; background: var(--inline-code); color: var(--strong); white-space: break-spaces; }
124+.prose hr { border: 0; border-top: 1px solid var(--line); margin: 32px 0; }
125+.prose blockquote { margin: 0 0 16px; padding: 2px 16px; border-left: 3px solid var(--line-2); color: var(--muted); }
126+.prose img { max-width: 100%; border-radius: 12px; border: 1px solid var(--line); }
127+
128+.code { position: relative; margin: 0 0 18px; border-radius: 12px; border: 1px solid var(--line); background: var(--code-bg); overflow: hidden; }
129+.code pre { margin: 0; padding: 16px 18px; overflow-x: auto; font-size: 13.5px; line-height: 1.6; background: transparent !important; }
130+.code-lang { position: absolute; top: 8px; left: 14px; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--faint); font-family: var(--font-mono), monospace; pointer-events: none; }
131+.code:has(.code-lang) pre { padding-top: 30px; }
132+.code-copy { position: absolute; top: 7px; right: 8px; font: inherit; font-size: 12px; padding: 3px 9px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--surface); color: var(--muted); cursor: pointer; opacity: 0; transition: opacity 0.15s; }
133+.code:hover .code-copy, .code-copy:focus-visible { opacity: 1; }
134+.shiki span { color: var(--shiki-dark); }
135+:root[data-theme="light"] .shiki span { color: var(--shiki-light); }
136+@media (prefers-color-scheme: light) { :root:not([data-theme]) .shiki span { color: var(--shiki-light); } }
137+
138+.table { margin: 0 0 18px; overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; }
139+.table table { width: 100%; border-collapse: collapse; font-size: 14px; }
140+.table th, .table td { padding: 9px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
141+.table th { font-weight: 600; color: var(--strong); background: var(--surface-2); }
142+.table tr:last-child td { border-bottom: 0; }
143+
144+.callout { margin: 0 0 18px; padding: 12px 16px; border-radius: 12px; border: 1px solid var(--line); border-left: 3px solid var(--note); background: var(--note-soft); }
145+.callout > :last-child { margin-bottom: 0; }
146+.callout-title { margin: 0 0 4px !important; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--note); }
147+.callout.tip { border-left-color: var(--tip); background: var(--tip-soft); } .callout.tip .callout-title { color: var(--tip); }
148+.callout.warning { border-left-color: var(--warn); background: var(--warn-soft); } .callout.warning .callout-title { color: var(--warn); }
149+.callout.danger { border-left-color: var(--danger); background: var(--danger-soft); } .callout.danger .callout-title { color: var(--danger); }
150+
151+.pager { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--line); }
152+.pager-link { display: flex; flex-direction: column; gap: 2px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface); color: var(--strong); text-decoration: none; font-weight: 600; font-size: 15px; }
153+.pager-link span { font-size: 12px; font-weight: 500; color: var(--faint); }
154+.pager-link:hover { border-color: color-mix(in srgb, var(--accent-light) 45%, transparent); }
155+.pager-link.next { text-align: right; grid-column: 2; }
156+
157+/* Error page and footer */
158+.error-page { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 64px 20px; }
159+.error-page img { object-fit: contain; margin-bottom: 18px; }
160+.error-code { margin: 0; font-family: var(--font-mono), monospace; font-weight: 600; letter-spacing: 0.2em; color: var(--accent-light); }
161+.error-page h1 { font-size: 34px; margin: 6px 0 10px; }
162+.error-page > p:not(.error-code) { max-width: 480px; color: var(--muted); margin: 0 0 24px; }
163+.footer { border-top: 1px solid var(--line); color: var(--faint); font-size: 13px; }
164+.footer-in { max-width: 1360px; margin: 0 auto; padding: 20px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
165+.footer-links { display: flex; gap: 16px; }
166+.footer-links a { color: var(--muted); text-decoration: none; }
167+.footer-links a:hover { color: var(--accent-light); }
168+
169+@media (max-width: 1180px) {
170+ .doc { grid-template-columns: 230px minmax(0, 1fr); }
171+ .toc { display: none; }
172+}
173+@media (max-width: 900px) {
174+ .header-in .tabs { display: none; }
175+ .tabs-mobile { display: flex; overflow-x: auto; padding: 0 12px 10px; max-width: 1360px; margin: 0 auto; }
176+ .search-button { min-width: 0; }
177+ .search-label, .search-button kbd, .header-right .site { display: none; }
178+ .doc { grid-template-columns: minmax(0, 1fr); }
179+ .sidebar { display: none; }
180+ .sidebar-mobile { display: block; margin: 0 0 18px; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface); }
181+ .sidebar-mobile summary { cursor: pointer; font-size: 14px; font-weight: 600; color: var(--strong); }
182+ .sidebar-mobile ul { margin-top: 8px; }
183+ .article { padding-top: 22px; }
184+ .prose .anchor { display: none; }
185+ .header .tabs-mobile a[aria-current="page"] { box-shadow: none; }
186+}
187+@media (max-width: 560px) {
188+ .tag { display: none; }
189+ .pager { grid-template-columns: 1fr; }
190+ .pager-link.next { grid-column: 1; }
191+}
Ajoutésrc/app/layout.tsx+48−0
@@ -0,0 +1,48 @@
1+import type { Metadata } from "next";
2+import { Inter, JetBrains_Mono, Outfit } from "next/font/google";
3+import { Header } from "@/components/Header";
4+import { CodeCopy } from "@/components/CodeCopy";
5+import { requireUrl } from "@/lib/env";
6+import { SECTIONS } from "@/lib/sections";
7+import "./globals.css";
8+
9+// Fonts downloaded at build time and served by the site: no request to Google from the pages.
10+const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
11+const outfit = Outfit({ subsets: ["latin"], weight: ["500", "600", "700", "800"], variable: "--font-outfit", display: "swap" });
12+const mono = JetBrains_Mono({ subsets: ["latin"], weight: ["400", "500"], variable: "--font-mono", display: "swap" });
13+
14+export const metadata: Metadata = {
15+ metadataBase: new URL(requireUrl("PUBLIC_URL")),
16+ title: { default: "Documentation — Dimdam's", template: "%s — Documentation Dimdam's" },
17+ description: "Documentation de Dimdam's : hébergement, API et CLI, Dimdam's Git et Dimdam's Status.",
18+ icons: { icon: "/icon.webp" },
19+};
20+
21+// Theme chosen by the visitor (localStorage), set before the first paint to avoid a flash.
22+const THEME_SCRIPT = `try{var t=localStorage.getItem("ds_theme");if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch(e){}`;
23+
24+export default function RootLayout({ children }: { children: React.ReactNode }) {
25+ const mainSite = requireUrl("MAIN_SITE_URL");
26+ return (
27+ <html lang="fr" className={`${inter.variable} ${outfit.variable} ${mono.variable}`} suppressHydrationWarning>
28+ <head>
29+ <script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />
30+ </head>
31+ <body>
32+ <Header sections={SECTIONS.map(({ id, title }) => ({ id, title }))} mainSite={mainSite} />
33+ {children}
34+ <footer className="footer">
35+ <div className="footer-in">
36+ <span>© 2021 – {new Date().getFullYear()} Dimdam&apos;s · Hébergé en France</span>
37+ <span className="footer-links">
38+ <a href={mainSite}>dimdams.com</a>
39+ <a href={requireUrl("STATUS_URL")}>État des services</a>
40+ <a href={requireUrl("GIT_URL")}>Dimdam&apos;s Git</a>
41+ </span>
42+ </div>
43+ </footer>
44+ <CodeCopy />
45+ </body>
46+ </html>
47+ );
48+}
Ajoutésrc/app/not-found.tsx+17−0
@@ -0,0 +1,17 @@
1+import Image from "next/image";
2+import Link from "next/link";
3+
4+export const metadata = { title: "Page introuvable", robots: { index: false } };
5+
6+/** 404 (out/404.html, served by nginx for an unknown address). */
7+export default function NotFound() {
8+ return (
9+ <main className="error-page">
10+ <Image src="/logo-mark.png" alt="" width={64} height={64} />
11+ <p className="error-code">404</p>
12+ <h1>Page introuvable</h1>
13+ <p>Cette page n&apos;existe pas, ou elle a été déplacée lors du passage à la nouvelle documentation. La recherche (Ctrl K) vous aidera à la retrouver.</p>
14+ <Link href="/" className="btn primary">Accueil de la documentation</Link>
15+ </main>
16+ );
17+}
Ajoutésrc/app/page.tsx+32−0
@@ -0,0 +1,32 @@
1+import Link from "next/link";
2+import { listPages } from "@/lib/content";
3+import { SECTIONS } from "@/lib/sections";
4+
5+/** Home: one card per documentation, with its first pages. */
6+export default function Home() {
7+ return (
8+ <main className="home">
9+ <section className="hero">
10+ <h1>Documentation Dimdam&apos;s</h1>
11+ <p>Tout pour héberger, automatiser et suivre vos projets sur Dimdam&apos;s. Choisissez une documentation, ou cherchez directement (Ctrl K).</p>
12+ </section>
13+ <div className="cards">
14+ {SECTIONS.map((s) => {
15+ const pages = listPages(s.id).filter((p) => p.slug !== "index");
16+ return (
17+ <Link key={s.id} href={`/${s.id}/`} className="card">
18+ <h2>{s.title}</h2>
19+ <p>{s.description}</p>
20+ <ul>
21+ {pages.slice(0, 4).map((p) => (
22+ <li key={p.slug}>{p.title}</li>
23+ ))}
24+ </ul>
25+ <span className="card-more">{pages.length} pages →</span>
26+ </Link>
27+ );
28+ })}
29+ </div>
30+ </main>
31+ );
32+}
Ajoutésrc/app/robots.ts+8−0
@@ -0,0 +1,8 @@
1+import type { MetadataRoute } from "next";
2+import { requireUrl } from "@/lib/env";
3+
4+export const dynamic = "force-static";
5+
6+export default function robots(): MetadataRoute.Robots {
7+ return { rules: { userAgent: "*", allow: "/" }, sitemap: `${requireUrl("PUBLIC_URL")}/sitemap.xml` };
8+}
Ajoutésrc/app/search-index.json/route.ts+21−0
@@ -0,0 +1,21 @@
1+import { allPages, getPage } from "@/lib/content";
2+import { sectionById } from "@/lib/sections";
3+
4+// /search-index.json, written at build time: what the search (src/components/Search.tsx) looks into.
5+export const dynamic = "force-static";
6+
7+export async function GET() {
8+ const entries = [];
9+ for (const meta of allPages()) {
10+ const page = await getPage(meta.section, meta.slug);
11+ if (!page) continue;
12+ entries.push({
13+ href: page.href,
14+ section: sectionById(page.section)!.title,
15+ title: page.slug === "index" ? `${sectionById(page.section)!.title} : présentation` : page.title,
16+ headings: page.toc.map(({ id, text }) => ({ id, text })),
17+ text: `${page.description} ${page.text}`.slice(0, 6000),
18+ });
19+ }
20+ return Response.json(entries);
21+}
Ajoutésrc/app/sitemap.ts+10−0
@@ -0,0 +1,10 @@
1+import type { MetadataRoute } from "next";
2+import { allPages } from "@/lib/content";
3+import { requireUrl } from "@/lib/env";
4+
5+export const dynamic = "force-static";
6+
7+export default function sitemap(): MetadataRoute.Sitemap {
8+ const base = requireUrl("PUBLIC_URL");
9+ return [{ url: `${base}/` }, ...allPages().map((p) => ({ url: `${base}${p.href}` }))];
10+}
Ajoutésrc/components/CodeCopy.tsx+24−0
@@ -0,0 +1,24 @@
1+"use client";
2+
3+import { useEffect } from "react";
4+
5+/** "Copier" buttons of the code blocks (rendered in the HTML by src/lib/markdown.ts). */
6+export function CodeCopy() {
7+ useEffect(() => {
8+ const onClick = async (event: MouseEvent) => {
9+ const button = (event.target as Element | null)?.closest?.(".code-copy") as HTMLButtonElement | null;
10+ if (!button) return;
11+ const code = button.parentElement?.querySelector("pre")?.innerText ?? "";
12+ try {
13+ await navigator.clipboard.writeText(code.replace(/\n$/, ""));
14+ button.textContent = "Copié";
15+ } catch {
16+ button.textContent = "Copie impossible";
17+ }
18+ setTimeout(() => (button.textContent = "Copier"), 1500);
19+ };
20+ document.addEventListener("click", onClick);
21+ return () => document.removeEventListener("click", onClick);
22+ }, []);
23+ return null;
24+}
Ajoutésrc/components/DocPage.tsx+76−0
@@ -0,0 +1,76 @@
1+import Link from "next/link";
2+import type { Page, PageMeta } from "@/lib/content";
3+import { sectionById } from "@/lib/sections";
4+
5+/** A page of a documentation: pages of the section on the left, the text, its outline on the right. */
6+export function DocPage({ page, pages }: { page: Page; pages: PageMeta[] }) {
7+ const section = sectionById(page.section)!;
8+ const at = pages.findIndex((p) => p.slug === page.slug);
9+ const previous = at > 0 ? pages[at - 1] : null;
10+ const next = at >= 0 && at < pages.length - 1 ? pages[at + 1] : null;
11+
12+ const nav = (
13+ <ul>
14+ {pages.map((p) => (
15+ <li key={p.slug}>
16+ <Link href={p.href} aria-current={p.slug === page.slug ? "page" : undefined}>
17+ {p.slug === "index" ? "Présentation" : p.title}
18+ </Link>
19+ </li>
20+ ))}
21+ </ul>
22+ );
23+
24+ return (
25+ <div className="doc">
26+ <aside className="sidebar" aria-label={`Pages : ${section.title}`}>
27+ <p className="sidebar-title">{section.title}</p>
28+ {nav}
29+ </aside>
30+
31+ <main className="article">
32+ <details className="sidebar-mobile">
33+ <summary>{section.title} : toutes les pages</summary>
34+ {nav}
35+ </details>
36+ <p className="breadcrumb">
37+ <Link href={`/${section.id}/`}>{section.title}</Link>
38+ {page.slug !== "index" && <> › {page.title}</>}
39+ </p>
40+ <h1>{page.title}</h1>
41+ {page.description && <p className="lead">{page.description}</p>}
42+ <div className="prose" dangerouslySetInnerHTML={{ __html: page.html }} />
43+
44+ <nav className="pager" aria-label="Page précédente et suivante">
45+ {previous ? (
46+ <Link href={previous.href} className="pager-link">
47+ <span>Précédent</span>
48+ {previous.slug === "index" ? "Présentation" : previous.title}
49+ </Link>
50+ ) : (
51+ <span />
52+ )}
53+ {next && (
54+ <Link href={next.href} className="pager-link next">
55+ <span>Suivant</span>
56+ {next.title}
57+ </Link>
58+ )}
59+ </nav>
60+ </main>
61+
62+ {page.toc.length > 1 && (
63+ <aside className="toc" aria-label="Sur cette page">
64+ <p className="toc-title">Sur cette page</p>
65+ <ul>
66+ {page.toc.map((t) => (
67+ <li key={t.id} className={t.depth === 3 ? "sub" : ""}>
68+ <a href={`#${t.id}`}>{t.text}</a>
69+ </li>
70+ ))}
71+ </ul>
72+ </aside>
73+ )}
74+ </div>
75+ );
76+}
Ajoutésrc/components/Header.tsx+39−0
@@ -0,0 +1,39 @@
1+"use client";
2+
3+import Image from "next/image";
4+import Link from "next/link";
5+import { usePathname } from "next/navigation";
6+import { Search } from "./Search";
7+import { ThemeToggle } from "./ThemeToggle";
8+
9+/** Header: logo, one tab per documentation, search, theme and the main site. */
10+export function Header({ sections, mainSite }: { sections: { id: string; title: string }[]; mainSite: string }) {
11+ const pathname = usePathname();
12+ const current = pathname.split("/")[1] ?? "";
13+ const tabs = sections.map((s) => (
14+ <Link key={s.id} href={`/${s.id}/`} aria-current={s.id === current ? "page" : undefined}>
15+ {s.title}
16+ </Link>
17+ ));
18+
19+ return (
20+ <header className="header">
21+ <div className="header-in">
22+ <Link href="/" className="brand">
23+ <Image src="/logo-mark.png" alt="" width={36} height={36} priority />
24+ <span>Dimdam&apos;s</span>
25+ <span className="tag">Docs</span>
26+ </Link>
27+ <nav className="tabs" aria-label="Documentations">{tabs}</nav>
28+ <div className="header-right">
29+ <Search />
30+ <ThemeToggle />
31+ <a className="btn primary site" href={mainSite}>
32+ dimdams.com
33+ </a>
34+ </div>
35+ </div>
36+ <nav className="tabs tabs-mobile" aria-label="Documentations">{tabs}</nav>
37+ </header>
38+ );
39+}
Ajoutésrc/components/Search.tsx+155−0
@@ -0,0 +1,155 @@
1+"use client";
2+
3+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4+import { useRouter } from "next/navigation";
5+
6+interface Entry {
7+ href: string;
8+ section: string;
9+ title: string;
10+ headings: { id: string; text: string }[];
11+ text: string;
12+}
13+
14+const norm = (s: string) =>
15+ s
16+ .normalize("NFD")
17+ .replace(/[̀-ͯ]/g, "")
18+ .toLowerCase();
19+
20+/** Best matches of the query: every word must appear; title, then headings, then text. */
21+function search(index: Entry[], query: string) {
22+ const words = norm(query).split(/\s+/).filter((w) => w.length > 1);
23+ if (words.length === 0) return [];
24+ const results: { entry: Entry; score: number; anchor: string | null; snippet: string }[] = [];
25+ for (const entry of index) {
26+ const title = norm(entry.title);
27+ const text = norm(entry.text);
28+ const headings = entry.headings.map((h) => ({ ...h, n: norm(h.text) }));
29+ if (!words.every((w) => title.includes(w) || text.includes(w) || headings.some((h) => h.n.includes(w)))) continue;
30+ let score = 0;
31+ for (const w of words) {
32+ if (title.includes(w)) score += 10;
33+ if (headings.some((h) => h.n.includes(w))) score += 4;
34+ if (text.includes(w)) score += 1;
35+ }
36+ const heading = headings.find((h) => words.every((w) => h.n.includes(w))) ?? headings.find((h) => words.some((w) => h.n.includes(w)));
37+ const at = text.indexOf(words[0]);
38+ const start = Math.max(0, at - 50);
39+ const snippet = at >= 0 ? `${start > 0 ? "…" : ""}${entry.text.slice(start, start + 150)}…` : entry.text.slice(0, 150);
40+ results.push({ entry, score, anchor: heading?.id ?? null, snippet });
41+ }
42+ return results.sort((a, b) => b.score - a.score).slice(0, 8);
43+}
44+
45+/** Search of the whole documentation (Ctrl+K or "/"), from the index built with the site. */
46+export function Search() {
47+ const router = useRouter();
48+ const [open, setOpen] = useState(false);
49+ const [query, setQuery] = useState("");
50+ const [index, setIndex] = useState<Entry[] | null>(null);
51+ const [cursor, setCursor] = useState(0);
52+ const input = useRef<HTMLInputElement>(null);
53+
54+ const show = useCallback(() => {
55+ setOpen(true);
56+ if (!index) {
57+ fetch("/search-index.json")
58+ .then((r) => r.json())
59+ .then((data: Entry[]) => setIndex(data))
60+ .catch(() => setIndex([]));
61+ }
62+ }, [index]);
63+
64+ useEffect(() => {
65+ const onKey = (e: KeyboardEvent) => {
66+ const typing = (e.target as HTMLElement | null)?.closest?.("input, textarea, [contenteditable]");
67+ if ((e.key === "k" && (e.ctrlKey || e.metaKey)) || (e.key === "/" && !typing)) {
68+ e.preventDefault();
69+ show();
70+ }
71+ };
72+ document.addEventListener("keydown", onKey);
73+ return () => document.removeEventListener("keydown", onKey);
74+ }, [show]);
75+
76+ useEffect(() => {
77+ if (!open) return;
78+ input.current?.focus();
79+ const overflow = document.body.style.overflow;
80+ document.body.style.overflow = "hidden";
81+ return () => {
82+ document.body.style.overflow = overflow;
83+ };
84+ }, [open]);
85+
86+ const results = useMemo(() => (index ? search(index, query) : []), [index, query]);
87+
88+ const close = () => {
89+ setOpen(false);
90+ setQuery("");
91+ setCursor(0);
92+ };
93+ const go = (r: (typeof results)[number]) => {
94+ close();
95+ router.push(r.anchor ? `${r.entry.href}#${r.anchor}` : r.entry.href);
96+ };
97+
98+ const onKeyDown = (e: React.KeyboardEvent) => {
99+ if (e.key === "Escape") close();
100+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
101+ e.preventDefault();
102+ if (results.length) setCursor((c) => (c + (e.key === "ArrowDown" ? 1 : results.length - 1)) % results.length);
103+ }
104+ if (e.key === "Enter" && results[cursor]) go(results[cursor]);
105+ };
106+
107+ return (
108+ <>
109+ <button type="button" className="search-button" onClick={show} aria-label="Rechercher dans la documentation">
110+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
111+ <circle cx="11" cy="11" r="7" />
112+ <path d="m20 20-3.5-3.5" />
113+ </svg>
114+ <span className="search-label">Rechercher</span>
115+ <kbd>Ctrl K</kbd>
116+ </button>
117+ {open && (
118+ <div className="search-overlay" role="dialog" aria-modal="true" aria-label="Rechercher" onMouseDown={(e) => e.target === e.currentTarget && close()}>
119+ <div className="search-box">
120+ <input
121+ ref={input}
122+ value={query}
123+ onChange={(e) => {
124+ setQuery(e.target.value);
125+ setCursor(0);
126+ }}
127+ onKeyDown={onKeyDown}
128+ placeholder="Rechercher : clé d'API, sauvegarde, SSH, CI…"
129+ aria-label="Rechercher"
130+ />
131+ <ul className="search-results">
132+ {index === null && <li className="search-empty">Chargement…</li>}
133+ {index && query.trim().length > 1 && results.length === 0 && <li className="search-empty">Aucun résultat pour « {query.trim()} ».</li>}
134+ {results.map((r, i) => (
135+ <li key={r.entry.href + (r.anchor ?? "")}>
136+ <button type="button" className={i === cursor ? "active" : ""} onMouseEnter={() => setCursor(i)} onClick={() => go(r)}>
137+ <span className="search-where">{r.entry.section}</span>
138+ <span className="search-title">
139+ {r.entry.title}
140+ {r.anchor && <span className="search-heading"> › {r.entry.headings.find((h) => h.id === r.anchor)?.text}</span>}
141+ </span>
142+ <span className="search-snippet">{r.snippet}</span>
143+ </button>
144+ </li>
145+ ))}
146+ </ul>
147+ <p className="search-help">
148+ <kbd>↑</kbd> <kbd>↓</kbd> pour choisir · <kbd>Entrée</kbd> pour ouvrir · <kbd>Échap</kbd> pour fermer
149+ </p>
150+ </div>
151+ </div>
152+ )}
153+ </>
154+ );
155+}
Ajoutésrc/components/ThemeToggle.tsx+44−0
@@ -0,0 +1,44 @@
1+"use client";
2+
3+import { useEffect, useState } from "react";
4+
5+type Theme = "light" | "dark";
6+
7+function currentTheme(): Theme {
8+ const set = document.documentElement.dataset.theme;
9+ if (set === "light" || set === "dark") return set;
10+ return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
11+}
12+
13+/** Light / dark switch, kept in this browser (localStorage). */
14+export function ThemeToggle() {
15+ const [theme, setTheme] = useState<Theme | null>(null);
16+ useEffect(() => setTheme(currentTheme()), []);
17+
18+ const toggle = () => {
19+ const next: Theme = currentTheme() === "dark" ? "light" : "dark";
20+ document.documentElement.dataset.theme = next;
21+ try {
22+ localStorage.setItem("ds_theme", next);
23+ } catch {
24+ // Storage refused (private browsing): the theme only lasts for this page.
25+ }
26+ setTheme(next);
27+ };
28+
29+ const label = theme === "light" ? "Passer au thème sombre" : "Passer au thème clair";
30+ return (
31+ <button type="button" className="btn icon" onClick={toggle} aria-label={label} title={label}>
32+ {theme === "light" ? (
33+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
34+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
35+ </svg>
36+ ) : (
37+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
38+ <circle cx="12" cy="12" r="4" />
39+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
40+ </svg>
41+ )}
42+ </button>
43+ );
44+}
Ajoutésrc/lib/content.ts+91−0
@@ -0,0 +1,91 @@
1+import fs from "fs";
2+import path from "path";
3+import { CONTENT_VARIABLES, requireUrl } from "./env";
4+import { renderMarkdown, type RenderedPage } from "./markdown";
5+import { SECTIONS, sectionById } from "./sections";
6+
7+// Pages of the documentation: content/<section>/<page>.md, with a header:
8+// ---
9+// title: Authentification
10+// description: Créer une clé et l'envoyer.
11+// order: 2
12+// ---
13+// "index.md" is the home of the section. {{MAIN_SITE_URL}} and the other names of CONTENT_VARIABLES
14+// are replaced by their value in the .env.
15+
16+const CONTENT_DIR = path.join(process.cwd(), "content");
17+
18+export interface PageMeta {
19+ section: string;
20+ slug: string;
21+ title: string;
22+ description: string;
23+ order: number;
24+ href: string;
25+}
26+
27+export interface Page extends PageMeta, RenderedPage {}
28+
29+function parseFile(file: string): { meta: Record<string, string>; body: string } {
30+ const raw = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
31+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
32+ if (!match) throw new Error(`[Contenu] ${file} : en-tête --- manquant`);
33+ const meta: Record<string, string> = {};
34+ for (const line of match[1].split("\n")) {
35+ const kv = line.match(/^([a-zA-Z]+):\s*(.*)$/);
36+ if (kv) meta[kv[1]] = kv[2].trim().replace(/^"(.*)"$/, "$1");
37+ }
38+ if (!meta.title) throw new Error(`[Contenu] ${file} : titre manquant`);
39+ return { meta, body: raw.slice(match[0].length) };
40+}
41+
42+function substitute(body: string, file: string): string {
43+ return body.replace(/\{\{([A-Z_]+)\}\}/g, (_all, name: string) => {
44+ if (!(CONTENT_VARIABLES as readonly string[]).includes(name)) throw new Error(`[Contenu] ${file} : variable inconnue {{${name}}}`);
45+ return requireUrl(name);
46+ });
47+}
48+
49+const hrefOf = (section: string, slug: string) => (slug === "index" ? `/${section}/` : `/${section}/${slug}/`);
50+
51+export function listPages(section: string): PageMeta[] {
52+ const dir = path.join(CONTENT_DIR, section);
53+ if (!sectionById(section) || !fs.existsSync(dir)) return [];
54+ return fs
55+ .readdirSync(dir)
56+ .filter((f) => f.endsWith(".md"))
57+ .map((f) => {
58+ const slug = f.slice(0, -3);
59+ const { meta } = parseFile(path.join(dir, f));
60+ return {
61+ section,
62+ slug,
63+ title: meta.title,
64+ description: meta.description ?? "",
65+ order: slug === "index" ? -1 : Number(meta.order ?? 999),
66+ href: hrefOf(section, slug),
67+ };
68+ })
69+ .sort((a, b) => a.order - b.order || a.title.localeCompare(b.title, "fr"));
70+}
71+
72+export async function getPage(section: string, slug: string): Promise<Page | null> {
73+ if (!/^[a-z0-9-]+$/.test(slug) || !sectionById(section)) return null;
74+ const file = path.join(CONTENT_DIR, section, `${slug}.md`);
75+ if (!fs.existsSync(file)) return null;
76+ const { meta, body } = parseFile(file);
77+ const rendered = await renderMarkdown(substitute(body, file));
78+ return {
79+ section,
80+ slug,
81+ title: meta.title,
82+ description: meta.description ?? "",
83+ order: Number(meta.order ?? 999),
84+ href: hrefOf(section, slug),
85+ ...rendered,
86+ };
87+}
88+
89+export function allPages(): PageMeta[] {
90+ return SECTIONS.flatMap((s) => listPages(s.id));
91+}
Ajoutésrc/lib/env.ts+17−0
@@ -0,0 +1,17 @@
1+// Settings of the documentation, all from the .env (no default value in the code).
2+
3+export function requireEnv(name: string): string {
4+ const value = process.env[name]?.trim();
5+ if (!value) throw new Error(`[Config] Variable ${name} manquante dans le .env`);
6+ return value;
7+}
8+
9+/** An address of the .env, without its final slash. */
10+export function requireUrl(name: string): string {
11+ const value = requireEnv(name).replace(/\/+$/, "");
12+ new URL(value);
13+ return value;
14+}
15+
16+/** Names usable in the Markdown pages as {{NAME}} (replaced at build time). */
17+export const CONTENT_VARIABLES = ["PUBLIC_URL", "MAIN_SITE_URL", "GIT_URL", "STATUS_URL"] as const;
Ajoutésrc/lib/markdown.ts+119−0
@@ -0,0 +1,119 @@
1+import { Marked, type Tokens } from "marked";
2+import { createHighlighter, type Highlighter } from "shiki";
3+
4+// Markdown of the pages (content/, written by Dimdam's: trusted) to HTML, at build time:
5+// headings with an anchor, code colored by Shiki (light and dark), callouts "> [!NOTE]".
6+
7+export interface TocEntry {
8+ id: string;
9+ text: string;
10+ depth: 2 | 3;
11+}
12+
13+export interface RenderedPage {
14+ html: string;
15+ toc: TocEntry[];
16+ /** Plain text, for the search index. */
17+ text: string;
18+}
19+
20+const LANGS = ["bash", "shell", "json", "javascript", "typescript", "yaml", "python", "ini", "diff", "http", "dockerfile", "text"];
21+const ALIASES: Record<string, string> = { sh: "bash", js: "javascript", ts: "typescript", yml: "yaml", console: "bash", env: "ini" };
22+const CALLOUTS: Record<string, string> = { NOTE: "Note", TIP: "Astuce", WARNING: "Attention", DANGER: "Important" };
23+
24+let highlighter: Promise<Highlighter> | null = null;
25+function getHighlighter() {
26+ highlighter ??= createHighlighter({ themes: ["github-light", "github-dark"], langs: LANGS });
27+ return highlighter;
28+}
29+
30+export function slugify(text: string): string {
31+ return text
32+ .normalize("NFD")
33+ .replace(/[̀-ͯ]/g, "")
34+ .toLowerCase()
35+ .replace(/<[^>]+>/g, "")
36+ .replace(/[^a-z0-9]+/g, "-")
37+ .replace(/^-+|-+$/g, "");
38+}
39+
40+const escapeHtml = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
41+const stripTags = (s: string) => s.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
42+
43+type CodeToken = Tokens.Code & { highlighted?: string };
44+type QuoteToken = Tokens.Blockquote & { callout?: string };
45+
46+export async function renderMarkdown(markdown: string): Promise<RenderedPage> {
47+ const hl = await getHighlighter();
48+ const toc: TocEntry[] = [];
49+ const used = new Map<string, number>();
50+ const uniqueId = (text: string) => {
51+ const base = slugify(text) || "section";
52+ const n = used.get(base) ?? 0;
53+ used.set(base, n + 1);
54+ return n ? `${base}-${n}` : base;
55+ };
56+
57+ const marked = new Marked({ gfm: true, async: true });
58+ marked.use({
59+ walkTokens(token) {
60+ if (token.type === "code") {
61+ const code = token as CodeToken;
62+ const lang = ALIASES[code.lang ?? ""] ?? (LANGS.includes(code.lang ?? "") ? code.lang! : "text");
63+ code.highlighted = hl.codeToHtml(code.text, { lang, themes: { light: "github-light", dark: "github-dark" }, defaultColor: false });
64+ }
65+ if (token.type === "blockquote") {
66+ const quote = token as QuoteToken;
67+ const first = quote.tokens[0];
68+ if (first?.type === "paragraph") {
69+ const marker = first.text.match(/^\[!(NOTE|TIP|WARNING|DANGER)\]\s*/);
70+ if (marker) {
71+ quote.callout = marker[1];
72+ first.text = first.text.slice(marker[0].length);
73+ const lead = first.tokens?.[0];
74+ if (lead && "text" in lead) {
75+ lead.text = lead.text.replace(/^\[!(NOTE|TIP|WARNING|DANGER)\]\s*/, "");
76+ if ("raw" in lead) lead.raw = lead.text;
77+ }
78+ }
79+ }
80+ }
81+ },
82+ renderer: {
83+ heading({ tokens, depth }) {
84+ const inner = this.parser.parseInline(tokens);
85+ const id = uniqueId(stripTags(inner));
86+ if (depth === 2 || depth === 3) toc.push({ id, text: stripTags(inner), depth });
87+ return `<h${depth} id="${id}"><a class="anchor" href="#${id}" aria-hidden="true" tabindex="-1">#</a>${inner}</h${depth}>\n`;
88+ },
89+ code(token) {
90+ const code = token as CodeToken;
91+ const label = code.lang ? `<span class="code-lang">${escapeHtml(code.lang)}</span>` : "";
92+ return `<div class="code">${label}<button type="button" class="code-copy" aria-label="Copier le code">Copier</button>${code.highlighted ?? `<pre><code>${escapeHtml(code.text)}</code></pre>`}</div>\n`;
93+ },
94+ blockquote(token) {
95+ const quote = token as QuoteToken;
96+ const body = this.parser.parse(quote.tokens);
97+ if (!quote.callout) return `<blockquote>${body}</blockquote>\n`;
98+ return `<div class="callout ${quote.callout.toLowerCase()}"><p class="callout-title">${CALLOUTS[quote.callout]}</p>${body}</div>\n`;
99+ },
100+ table(token) {
101+ const head = token.header.map((cell) => `<th${cell.align ? ` style="text-align:${cell.align}"` : ""}>${this.parser.parseInline(cell.tokens)}</th>`).join("");
102+ const rows = token.rows
103+ .map((row) => `<tr>${row.map((cell) => `<td${cell.align ? ` style="text-align:${cell.align}"` : ""}>${this.parser.parseInline(cell.tokens)}</td>`).join("")}</tr>`)
104+ .join("");
105+ return `<div class="table"><table><thead><tr>${head}</tr></thead><tbody>${rows}</tbody></table></div>\n`;
106+ },
107+ link({ href, title, tokens }) {
108+ const inner = this.parser.parseInline(tokens);
109+ const external = /^https?:\/\//.test(href);
110+ const attrs = `${title ? ` title="${escapeHtml(title)}"` : ""}${external ? ' target="_blank" rel="noopener noreferrer"' : ""}`;
111+ return `<a href="${escapeHtml(href)}"${attrs}>${inner}</a>`;
112+ },
113+ },
114+ });
115+
116+ const html = await marked.parse(markdown);
117+ const text = stripTags(html).replace(/\s+/g, " ").trim();
118+ return { html, toc, text };
119+}
Ajoutésrc/lib/sections.ts+32−0
@@ -0,0 +1,32 @@
1+// The documentations, one per product: a tab in the header and a folder in content/.
2+
3+export interface Section {
4+ id: string;
5+ title: string;
6+ description: string;
7+}
8+
9+export const SECTIONS: Section[] = [
10+ {
11+ id: "hebergement",
12+ title: "Hébergement",
13+ description: "Commander, configurer et gérer vos services : sites web, bots, serveurs de jeu, VPS et bases de données.",
14+ },
15+ {
16+ id: "api",
17+ title: "API & CLI",
18+ description: "Piloter vos services depuis vos scripts et vos pipelines avec l'API REST et la ligne de commande dimdams.",
19+ },
20+ {
21+ id: "git",
22+ title: "Dimdam's Git",
23+ description: "Héberger vos dépôts en France : code, collaboration, CI, paquets et sites Pages.",
24+ },
25+ {
26+ id: "status",
27+ title: "Dimdam's Status",
28+ description: "Suivre l'état des services Dimdam's et l'intégrer dans vos outils.",
29+ },
30+];
31+
32+export const sectionById = (id: string) => SECTIONS.find((s) => s.id === id);
Ajoutétsconfig.json+21−0
@@ -0,0 +1,21 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2022",
4+ "lib": ["dom", "dom.iterable", "esnext"],
5+ "allowJs": false,
6+ "skipLibCheck": true,
7+ "strict": true,
8+ "noEmit": true,
9+ "esModuleInterop": true,
10+ "module": "esnext",
11+ "moduleResolution": "bundler",
12+ "resolveJsonModule": true,
13+ "isolatedModules": true,
14+ "jsx": "react-jsx",
15+ "incremental": true,
16+ "plugins": [{ "name": "next" }],
17+ "paths": { "@/*": ["./src/*"] }
18+ },
19+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
20+ "exclude": ["node_modules"]
21+}