diff --git a/.gitignore b/.gitignore
index be7cacb..5012480 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,6 @@ llm-api/*.pyc
# Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25
/chroma-index/
+# Téléchargements + WebP générés par strapi_extraction/media-sync/ (poids important)
+strapi_extraction/extract/media-sync-work/
+
diff --git a/app/components/ContentSection.tsx b/app/components/ContentSection.tsx
index fb2a531..a0a0e94 100644
--- a/app/components/ContentSection.tsx
+++ b/app/components/ContentSection.tsx
@@ -6,6 +6,7 @@ import { fetchData } from "../utils/fetchData";
import { getApiUrl } from "../utils/getApiUrl";
import Carousel from "./Carousel";
import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
interface ImageData {
url: string;
@@ -211,7 +212,7 @@ export default function ContentSection({
prose-li:marker:text-primary
prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full prose-hr:my-6"
>
- {richText}
+ {richText}
)}
diff --git a/docs-site-interne/06-strapi-extraction.md b/docs-site-interne/06-strapi-extraction.md
index f3cb9da..eb1131c 100644
--- a/docs-site-interne/06-strapi-extraction.md
+++ b/docs-site-interne/06-strapi-extraction.md
@@ -1,6 +1,6 @@
# Outils `strapi_extraction/`
-**Dernière mise à jour :** 2026-04-22
+**Dernière mise à jour :** 2026-04-28
Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les
données issues de l'API Strapi en base de connaissance chatbot (hors runtime
@@ -84,8 +84,18 @@ comme source de vérité sans comparer au CMS.
Ces points seront corrigés en même temps que l'enrichissement du vault
(glossaire + homepage Strapi → notes `40-Glossaire/` et `30-Parcours/`).
+## Sync médias WebP (hors pipeline GrasBot)
+
+Dossier **`strapi_extraction/media-sync/`** — inventaire des fichiers image liés aux
+content-types (`projects`, `competences`, `homepages`, `realisation-ias`, `glossaires`),
+téléchargement classé par rubrique, conversion WebP (sharp), puis ré-upload optionnel.
+
+Documentation : voir `strapi_extraction/media-sync/README.md`.
+Sortie lourde (ignorée Git) : `strapi_extraction/extract/media-sync-work/`.
+
## Liens complémentaires
- Vault + retrieval : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md)
- API LLM : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md)
- Schémas Strapi : [`03-cms-strapi.md`](./03-cms-strapi.md)
+- Performances images (audit) : [`09-performances-images.md`](./09-performances-images.md)
diff --git a/package-lock.json b/package-lock.json
index c3bf24f..a6e9f48 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
+ "sharp": "^0.33.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
@@ -47,9 +48,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
- "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -57,9 +58,9 @@
}
},
"node_modules/@img/colour": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
- "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"engines": {
@@ -67,12 +68,13 @@
}
},
"node_modules/@img/sharp-darwin-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
- "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -85,16 +87,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
- "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -107,16 +110,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.2.4"
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
- "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -127,12 +131,13 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
- "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -143,12 +148,13 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
- "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -159,12 +165,13 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
- "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -207,12 +214,13 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
- "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -223,12 +231,13 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
- "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -239,12 +248,13 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
- "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -255,12 +265,13 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
- "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -271,12 +282,13 @@
}
},
"node_modules/@img/sharp-linux-arm": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
- "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -289,16 +301,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.2.4"
+ "@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
- "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -311,7 +324,7 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.2.4"
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
@@ -359,12 +372,13 @@
}
},
"node_modules/@img/sharp-linux-s390x": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
- "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -377,16 +391,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.2.4"
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
- "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -399,16 +414,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.2.4"
+ "@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
- "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -421,16 +437,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
- "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -443,20 +460,21 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
- "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
+ "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
- "@emnapi/runtime": "^1.7.0"
+ "@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -485,12 +503,13 @@
}
},
"node_modules/@img/sharp-win32-ia32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
- "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
+ "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -504,12 +523,13 @@
}
},
"node_modules/@img/sharp-win32-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
- "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1164,6 +1184,20 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1182,6 +1216,17 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -1276,8 +1321,8 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "devOptional": true,
"license": "Apache-2.0",
- "optional": true,
"engines": {
"node": ">=8"
}
@@ -1860,6 +1905,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3058,6 +3110,367 @@
}
}
},
+ "node_modules/next/node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/next/node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -3086,6 +3499,51 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/next/node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -3713,8 +4171,8 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "devOptional": true,
"license": "ISC",
- "optional": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -3723,16 +4181,16 @@
}
},
"node_modules/sharp": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
- "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
- "optional": true,
"dependencies": {
- "@img/colour": "^1.0.0",
- "detect-libc": "^2.1.2",
- "semver": "^7.7.3"
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -3741,30 +4199,25 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.34.5",
- "@img/sharp-darwin-x64": "0.34.5",
- "@img/sharp-libvips-darwin-arm64": "1.2.4",
- "@img/sharp-libvips-darwin-x64": "1.2.4",
- "@img/sharp-libvips-linux-arm": "1.2.4",
- "@img/sharp-libvips-linux-arm64": "1.2.4",
- "@img/sharp-libvips-linux-ppc64": "1.2.4",
- "@img/sharp-libvips-linux-riscv64": "1.2.4",
- "@img/sharp-libvips-linux-s390x": "1.2.4",
- "@img/sharp-libvips-linux-x64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
- "@img/sharp-linux-arm": "0.34.5",
- "@img/sharp-linux-arm64": "0.34.5",
- "@img/sharp-linux-ppc64": "0.34.5",
- "@img/sharp-linux-riscv64": "0.34.5",
- "@img/sharp-linux-s390x": "0.34.5",
- "@img/sharp-linux-x64": "0.34.5",
- "@img/sharp-linuxmusl-arm64": "0.34.5",
- "@img/sharp-linuxmusl-x64": "0.34.5",
- "@img/sharp-wasm32": "0.34.5",
- "@img/sharp-win32-arm64": "0.34.5",
- "@img/sharp-win32-ia32": "0.34.5",
- "@img/sharp-win32-x64": "0.34.5"
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/shebang-command": {
@@ -3872,6 +4325,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
diff --git a/package.json b/package.json
index a78d7fd..b7e2f9c 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,11 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "media:inventory": "node strapi_extraction/media-sync/01-fetch-inventory.js",
+ "media:download": "node strapi_extraction/media-sync/02-download.js",
+ "media:webp": "node strapi_extraction/media-sync/03-convert-webp.js",
+ "media:upload": "node strapi_extraction/media-sync/04-upload-replace.js"
},
"dependencies": {
"@strapi/blocks-react-renderer": "^1.0.1",
@@ -31,6 +35,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
+ "sharp": "^0.33.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
diff --git a/strapi_extraction/media-sync/01-fetch-inventory.js b/strapi_extraction/media-sync/01-fetch-inventory.js
new file mode 100644
index 0000000..7831519
--- /dev/null
+++ b/strapi_extraction/media-sync/01-fetch-inventory.js
@@ -0,0 +1,139 @@
+/**
+ * 01 — Appelle l’API Strapi (lecture publique) et produit media-inventory.json
+ * (liste de tous les fichiers image liés aux content-types configurés).
+ *
+ * Usage : node strapi_extraction/media-sync/01-fetch-inventory.js
+ */
+const fs = require("fs");
+const path = require("path");
+const {
+ API_BASE,
+ WORK_ROOT,
+ FILE_INVENTORY,
+ COLLECTIONS,
+ PAGE_SIZE,
+} = require("./config");
+const { normalizeField, safeSlug } = require("./lib/collect-media");
+
+function unwrapEntry(entry) {
+ if (!entry) return null;
+ if (entry.attributes) {
+ return {
+ ...entry.attributes,
+ id: entry.id,
+ documentId: entry.documentId,
+ };
+ }
+ return entry;
+}
+
+async function fetchJson(url) {
+ const r = await fetch(url);
+ if (!r.ok) {
+ const txt = await r.text();
+ throw new Error(`HTTP ${r.status} ${url}\n${txt.slice(0, 500)}`);
+ }
+ return r.json();
+}
+
+async function fetchAllEntries(plural) {
+ const out = [];
+ let page = 1;
+ /* eslint-disable no-constant-condition */
+ while (true) {
+ const params = new URLSearchParams();
+ params.set("pagination[page]", String(page));
+ params.set("pagination[pageSize]", String(PAGE_SIZE));
+ /**
+ * Strapi v5 : populate[picture]=* peut provoquer « Invalid key related » selon versions.
+ * populate=* hydrate les relations premier niveau (y compris les médias).
+ */
+ params.set("populate", "*");
+
+ const url = `${API_BASE}/${plural}?${params}`;
+ const json = await fetchJson(url);
+ const rows = Array.isArray(json.data) ? json.data : [];
+ for (const row of rows) {
+ out.push(unwrapEntry(row));
+ }
+ const pageCount = json.meta?.pagination?.pageCount ?? 1;
+ if (page >= pageCount || rows.length === 0) break;
+ page += 1;
+ await new Promise((r) => setTimeout(r, 200));
+ }
+ return out;
+}
+
+async function main() {
+ console.log("🔍 STRAPI_URL (origine) →", require("./config").STRAPI_URL);
+ console.log("🔍 API_BASE →", API_BASE);
+
+ if (!fs.existsSync(WORK_ROOT)) {
+ fs.mkdirSync(WORK_ROOT, { recursive: true });
+ }
+
+ const inventory = {
+ generatedAt: new Date().toISOString(),
+ apiBase: API_BASE,
+ files: [],
+ };
+
+ /** @type {typeof inventory.files} */
+ const records = [];
+
+ for (const col of COLLECTIONS) {
+ console.log(`\n📂 ${col.plural} (${col.section})…`);
+ let entries;
+ try {
+ entries = await fetchAllEntries(col.plural);
+ } catch (e) {
+ console.error(` ⚠️ Endpoint indisponible ou erreur : ${e.message}`);
+ continue;
+ }
+
+ console.log(` ${entries.length} entrée(s)`);
+
+ for (const entry of entries) {
+ if (!entry) continue;
+ const slug = safeSlug(entry);
+
+ for (const field of col.fields) {
+ const items = normalizeField(entry[field.name], field.multiple);
+ for (const { file, index } of items) {
+ const base = path.basename(file.url.split("?")[0] || "file");
+ records.push({
+ collectionPlural: col.plural,
+ collectionSingular: col.singular,
+ strapiRef: col.ref,
+ section: col.section,
+ entrySlug: slug,
+ entryId: entry.id,
+ entryDocumentId: entry.documentId ?? null,
+ fieldName: field.name,
+ fieldMultiple: field.multiple,
+ fieldIndex: index,
+ fileId: file.id,
+ fileDocumentId: file.documentId ?? null,
+ filename: file.name || base,
+ url: file.url,
+ mime: file.mime,
+ size: file.size,
+ ext: file.ext,
+ /** chemin relatif WORK_ROOT/downloaded/... rempli par 02 */
+ relativeDownloadPath: null,
+ });
+ }
+ }
+ }
+ }
+
+ inventory.files = records.slice().sort((a, b) => a.fileId - b.fileId);
+
+ fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8");
+ console.log(`\n✅ Inventaire : ${inventory.files.length} fichier(s) → ${FILE_INVENTORY}`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/strapi_extraction/media-sync/02-download.js b/strapi_extraction/media-sync/02-download.js
new file mode 100644
index 0000000..c66f74a
--- /dev/null
+++ b/strapi_extraction/media-sync/02-download.js
@@ -0,0 +1,108 @@
+/**
+ * 02 — Télécharge chaque fichier unique de l’inventaire vers
+ * extract/media-sync-work/downloaded/{section}/{slug}/...
+ * et met à jour relativeDownloadPath dans media-inventory.json
+ *
+ * Usage : node strapi_extraction/media-sync/02-download.js
+ */
+const fs = require("fs");
+const path = require("path");
+const { STRAPI_URL, FILE_INVENTORY, WORK_ROOT, DIR_DOWNLOADED } = require("./config");
+
+function safeFilePart(name) {
+ return String(name || "file")
+ .replace(/[<>:"/\\|?*\x00-\x1f]/g, "_")
+ .slice(0, 180);
+}
+
+function absoluteUrl(relativeOrAbsolute) {
+ if (relativeOrAbsolute.startsWith("http://") || relativeOrAbsolute.startsWith("https://")) {
+ return relativeOrAbsolute;
+ }
+ return `${STRAPI_URL}${relativeOrAbsolute.startsWith("/") ? "" : "/"}${relativeOrAbsolute}`;
+}
+
+async function downloadBuffer(url) {
+ const r = await fetch(url);
+ if (!r.ok) {
+ throw new Error(`HTTP ${r.status} ${url}`);
+ }
+ const buf = Buffer.from(await r.arrayBuffer());
+ return buf;
+}
+
+async function main() {
+ if (!fs.existsSync(FILE_INVENTORY)) {
+ console.error("Manque l’inventaire. Lance d’abord : 01-fetch-inventory.js");
+ process.exit(1);
+ }
+
+ const raw = fs.readFileSync(FILE_INVENTORY, "utf8");
+ const inventory = JSON.parse(raw);
+ const files = inventory.files;
+ if (!Array.isArray(files) || files.length === 0) {
+ console.log("Inventaire vide — rien à télécharger.");
+ return;
+ }
+
+ if (!fs.existsSync(DIR_DOWNLOADED)) {
+ fs.mkdirSync(DIR_DOWNLOADED, { recursive: true });
+ }
+
+ const byId = new Map();
+ let ok = 0;
+ let skipped = 0;
+ let fail = 0;
+
+ for (const row of files) {
+ const id = row.fileId;
+ if (byId.has(id)) {
+ row.relativeDownloadPath = byId.get(id);
+ skipped++;
+ continue;
+ }
+
+ const rel = path.join(
+ row.section,
+ row.entrySlug,
+ `${id}_${safeFilePart(row.filename)}`
+ );
+ const abs = path.join(DIR_DOWNLOADED, rel);
+
+ try {
+ const url = absoluteUrl(row.url);
+ const buf = await downloadBuffer(url);
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
+ fs.writeFileSync(abs, buf);
+ row.relativeDownloadPath = rel.replace(/\\/g, "/");
+ byId.set(id, row.relativeDownloadPath);
+ ok++;
+ console.log(`✅ ${rel} (${(buf.length / 1024).toFixed(1)} KB)`);
+ } catch (e) {
+ console.error(`❌ fileId=${id} : ${e.message}`);
+ row.relativeDownloadPath = null;
+ row.downloadError = String(e.message);
+ fail++;
+ }
+ }
+
+ inventory.downloadedAt = new Date().toISOString();
+ inventory.stats = {
+ uniqueFiles: byId.size,
+ rowsTotal: files.length,
+ downloadedOk: ok,
+ dedupSkipped: skipped,
+ failed: fail,
+ };
+
+ fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8");
+ console.log(`\n📁 Base téléchargements : ${DIR_DOWNLOADED}`);
+ console.log(
+ `Résumé : ${ok} téléchargement(s), ${skipped} lignes réutilisent un fichier déjà pris, ${fail} échec(s).`
+ );
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/strapi_extraction/media-sync/03-convert-webp.js b/strapi_extraction/media-sync/03-convert-webp.js
new file mode 100644
index 0000000..194d9ee
--- /dev/null
+++ b/strapi_extraction/media-sync/03-convert-webp.js
@@ -0,0 +1,133 @@
+/**
+ * 03 — Convertit les téléchargements en WebP (sharp) sous
+ * extract/media-sync-work/webp/{section}/{slug}/{fileId}_{stem}.webp
+ * Les SVG sont copiés en .svg (sans conversion raster).
+ *
+ * Usage : node strapi_extraction/media-sync/03-convert-webp.js
+ */
+const fs = require("fs");
+const path = require("path");
+const sharp = require("sharp");
+const { FILE_INVENTORY, DIR_DOWNLOADED, DIR_WEBP, WORK_ROOT } = require("./config");
+
+const MAX_EDGE = 2560;
+const WEBP_QUALITY = 82;
+
+function stemFromFilename(filename) {
+ const b = path.basename(filename);
+ const i = b.lastIndexOf(".");
+ return i <= 0 ? b : b.slice(0, i);
+}
+
+async function toWebpRaster(srcPath, destPath) {
+ const meta = await sharp(srcPath).metadata();
+ const w = meta.width || 0;
+ const h = meta.height || 0;
+ let pipeline = sharp(srcPath).rotate();
+ if (w > MAX_EDGE || h > MAX_EDGE) {
+ pipeline = pipeline.resize(MAX_EDGE, MAX_EDGE, {
+ fit: "inside",
+ withoutEnlargement: true,
+ });
+ }
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
+ await pipeline.webp({ quality: WEBP_QUALITY, effort: 4 }).toFile(destPath);
+}
+
+async function main() {
+ if (!fs.existsSync(FILE_INVENTORY)) {
+ console.error("Manque media-inventory.json — lance 01 puis 02.");
+ process.exit(1);
+ }
+
+ const inventory = JSON.parse(fs.readFileSync(FILE_INVENTORY, "utf8"));
+ const files = inventory.files;
+ if (!Array.isArray(files) || !files.length) {
+ console.log("Rien à convertir.");
+ return;
+ }
+
+ if (!fs.existsSync(WORK_ROOT)) fs.mkdirSync(WORK_ROOT, { recursive: true });
+ if (!fs.existsSync(DIR_WEBP)) fs.mkdirSync(DIR_WEBP, { recursive: true });
+
+ const byId = new Map();
+ let ok = 0;
+ let err = 0;
+
+ for (const row of files) {
+ const id = row.fileId;
+ if (!row.relativeDownloadPath) {
+ row.relativeWebpPath = null;
+ continue;
+ }
+ if (byId.has(id)) {
+ row.relativeWebpPath = byId.get(id);
+ continue;
+ }
+
+ const src = path.join(DIR_DOWNLOADED, row.relativeDownloadPath);
+ if (!fs.existsSync(src)) {
+ row.relativeWebpPath = null;
+ row.convertError = "fichier téléchargé manquant";
+ err++;
+ continue;
+ }
+
+ const ext = path.extname(src).toLowerCase();
+ const baseStem = `${id}_${stemFromFilename(row.filename)}`;
+ const relDir = path.join(row.section, row.entrySlug);
+ let relOutFile;
+ let absOut;
+
+ try {
+ if (ext === ".svg") {
+ relOutFile = path.join(relDir, `${baseStem}.svg`).replace(/\\/g, "/");
+ absOut = path.join(DIR_WEBP, relOutFile);
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
+ fs.copyFileSync(src, absOut);
+ console.log(`svg-copy → ${relOutFile}`);
+ } else if (ext === ".webp") {
+ relOutFile = path.join(relDir, `${baseStem}.webp`).replace(/\\/g, "/");
+ absOut = path.join(DIR_WEBP, relOutFile);
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
+ fs.copyFileSync(src, absOut);
+ console.log(`webp-copy → ${relOutFile}`);
+ } else if ([".png", ".jpg", ".jpeg", ".tif", ".tiff"].includes(ext)) {
+ relOutFile = path.join(relDir, `${baseStem}.webp`).replace(/\\/g, "/");
+ absOut = path.join(DIR_WEBP, relOutFile);
+ await toWebpRaster(src, absOut);
+ console.log(`webp-transform → ${relOutFile}`);
+ } else {
+ relOutFile = path.join(relDir, path.basename(src)).replace(/\\/g, "/");
+ absOut = path.join(DIR_WEBP, relOutFile);
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
+ fs.copyFileSync(src, absOut);
+ console.log(`raw-copy → ${relOutFile}`);
+ }
+
+ row.relativeWebpPath = relOutFile;
+ byId.set(id, relOutFile);
+ ok++;
+ } catch (e) {
+ row.relativeWebpPath = null;
+ row.convertError = String(e.message);
+ err++;
+ console.error(`❌ fileId ${id}: ${e.message}`);
+ }
+ }
+
+ inventory.convertedAt = new Date().toISOString();
+ inventory.convertSettings = {
+ maxEdgePx: MAX_EDGE,
+ webpQuality: WEBP_QUALITY,
+ };
+
+ fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8");
+ console.log(`\n✅ Fichiers uniques traités : ${ok}, erreurs : ${err}`);
+ console.log(`📁 ${DIR_WEBP}`);
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/strapi_extraction/media-sync/04-upload-replace.js b/strapi_extraction/media-sync/04-upload-replace.js
new file mode 100644
index 0000000..cdbcd25
--- /dev/null
+++ b/strapi_extraction/media-sync/04-upload-replace.js
@@ -0,0 +1,270 @@
+/**
+ * 04 — Ré-upload les WebP générés vers Strapi et remplace les entrées média dans
+ * les fiches concernées (API authentifiée).
+ *
+ * Requiert dans l’env : STRAPI_API_TOKEN (JWT Strapi avec droits upload + mise à jour
+ * des collection types utilisés ; typiquement un jeton Full access en local).
+ *
+ * Par défaut : **dry-run** (aucune mutation).
+ * Mutation réelle : node strapi_extraction/media-sync/04-upload-replace.js --execute
+ *
+ * DANGER : sauvegarder au préalable votre base Strapi / médias. Tester sur une
+ * copie locale (Strapi localhost + même base si possible).
+ */
+const fs = require("fs");
+const path = require("path");
+require("dotenv").config({
+ path: path.join(__dirname, "../../cmsbackend/.env"),
+});
+require("dotenv").config({
+ path: path.join(__dirname, "../../.env.local"),
+});
+
+const {
+ FILE_INVENTORY,
+ DIR_WEBP,
+ API_BASE,
+} = require("./config");
+
+const TOKEN = process.env.STRAPI_API_TOKEN;
+const EXECUTE = process.argv.includes("--execute");
+
+function mimeForFile(filePath) {
+ const ext = path.extname(filePath).toLowerCase();
+ const map = {
+ ".webp": "image/webp",
+ ".svg": "image/svg+xml",
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ };
+ return map[ext] || "application/octet-stream";
+}
+
+/** Compte combien de lignes d’inventaire pointent le même fileId */
+function countRefs(files) {
+ const c = {};
+ for (const r of files) {
+ c[r.fileId] = (c[r.fileId] || 0) + 1;
+ }
+ return c;
+}
+
+/**
+ * Nom réservé multipart (ByteString) : les noms avec caractères hors U+00–U+FF
+ * (ex. U+2194 flèches dans des noms générés) provoquent l’erreur Node
+ * « Cannot convert argument to a ByteString » sur form.append(..., filename).
+ */
+function asciiUploadName(filePath, fileId) {
+ const ext = path.extname(filePath).toLowerCase();
+ const extOk = /^\.(webp|svg|png|jpe?g|gif|avif)$/.test(ext) ? ext : ".webp";
+ return `upload-${fileId}${extOk}`;
+}
+
+async function postUpload(filePath, fileId) {
+ const buf = fs.readFileSync(filePath);
+ const asciiName = asciiUploadName(filePath, fileId);
+ const blob = new Blob([buf], { type: mimeForFile(filePath) });
+ const body = new FormData();
+ body.append("files", blob, asciiName);
+
+ const res = await fetch(`${API_BASE}/upload`, {
+ method: "POST",
+ headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {},
+ body,
+ });
+ if (!res.ok) {
+ const t = await res.text();
+ throw new Error(`POST /upload ${res.status} ${t.slice(0, 800)}`);
+ }
+ const json = await res.json();
+ const arr = Array.isArray(json) ? json : json?.data;
+ if (!Array.isArray(arr) || !arr[0]) {
+ throw new Error(`Réponse upload inattendue : ${JSON.stringify(json).slice(0, 400)}`);
+ }
+ return arr[0];
+}
+
+function unwrapEntry(raw) {
+ if (!raw) return null;
+ if (raw.data) return unwrapEntry(raw.data);
+ if (raw.attributes) {
+ return {
+ ...raw.attributes,
+ id: raw.id,
+ documentId: raw.documentId,
+ };
+ }
+ return raw;
+}
+
+async function getEntryPlural(plural, documentId) {
+ const qs = new URLSearchParams({ populate: "*" });
+ const url = `${API_BASE}/${plural}/${documentId}?${qs}`;
+ const res = await fetch(url, {
+ headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {},
+ });
+ if (!res.ok) {
+ const t = await res.text();
+ throw new Error(`GET ${url} → ${res.status} ${t.slice(0, 400)}`);
+ }
+ const json = await res.json();
+ return unwrapEntry(json);
+}
+
+function getMediaIdsFromField(entry, fieldName, multiple) {
+ const v = entry[fieldName];
+ if (multiple) {
+ const arr = Array.isArray(v) ? v : [];
+ return arr.map((x) => (typeof x === "object" && x !== null ? x.id : x)).filter(Boolean);
+ }
+ if (!v) return [];
+ if (typeof v === "object" && v.id) return [v.id];
+ if (typeof v === "number") return [v];
+ return [];
+}
+
+async function putEntry(plural, documentId, payloadData) {
+ const url = `${API_BASE}/${plural}/${documentId}`;
+ const res = await fetch(url, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
+ },
+ body: JSON.stringify({ data: payloadData }),
+ });
+ if (!res.ok) {
+ const t = await res.text();
+ throw new Error(`PUT ${url} → ${res.status} ${t.slice(0, 800)}`);
+ }
+ return res.json();
+}
+
+async function deleteFile(fileId) {
+ const url = `${API_BASE}/upload/files/${fileId}`;
+ const res = await fetch(url, {
+ method: "DELETE",
+ headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {},
+ });
+ if (!res.ok) {
+ const t = await res.text();
+ throw new Error(`DELETE ${url} → ${res.status} ${t.slice(0, 400)}`);
+ }
+}
+
+async function main() {
+ if (!fs.existsSync(FILE_INVENTORY)) {
+ console.error("Manque media-inventory.json");
+ process.exit(1);
+ }
+
+ if (!TOKEN) {
+ console.error(
+ "STRAPI_API_TOKEN manquant. Crée un jeton API dans Strapi (Settings → API Tokens), puis exporte :\n" +
+ " Windows PowerShell : $env:STRAPI_API_TOKEN=\"…\"\n" +
+ " ou ajoute STRAPI_API_TOKEN dans cmsbackend/.env (non commité)."
+ );
+ process.exit(1);
+ }
+
+ const inventory = JSON.parse(fs.readFileSync(FILE_INVENTORY, "utf8"));
+ const files = inventory.files || [];
+ const refCount = countRefs(files);
+
+ const candidates = files.filter(
+ (r) =>
+ r.relativeWebpPath &&
+ r.relativeWebpPath.endsWith(".webp") &&
+ r.entryDocumentId
+ );
+
+ console.log(
+ `Lignes inventaire : ${files.length} — candidats WebP remplaçables (avec documentId) : ${candidates.length}`
+ );
+ console.log(`Mode : ${EXECUTE ? "EXECUTE (mutations réelles)" : "DRY-RUN (aucune mutation)"}\n`);
+
+ if (!EXECUTE) {
+ console.log(
+ "Exemple d’actions qui seraient effectuées avec --execute :\n" +
+ " 1. POST /api/upload pour chaque fichier .webp sous webp/\n" +
+ " 2. PUT /api/:collection/:documentId avec le champ média mis à jour (ids)\n" +
+ " 3. DELETE /api/upload/files/:oldFileId uniquement si l’ancien id n’a qu’une référence dans l’inventaire\n"
+ );
+ for (let i = 0; i < Math.min(5, candidates.length); i++) {
+ const r = candidates[i];
+ console.log(
+ ` • fileId=${r.fileId} → ${r.relativeWebpPath} → entrée ${r.collectionPlural} documentId=${r.entryDocumentId} champ=${r.fieldName}[${r.fieldIndex}]`
+ );
+ }
+ console.log("\nAjoute --execute pour réellement téléverser (après avoir testé la sauvegarde).");
+ return;
+ }
+
+ /** Par sécurité, traite fichier par fichier ; re-fetch après chaque succès évite désalignement indices */
+ for (const row of candidates) {
+ const src = path.join(DIR_WEBP, row.relativeWebpPath.replace(/\//g, path.sep));
+ if (!fs.existsSync(src)) {
+ console.warn(`SKIP fileId=${row.fileId} : fichier absent ${src}`);
+ continue;
+ }
+
+ const docId = row.entryDocumentId;
+ const plural = row.collectionPlural;
+ const field = row.fieldName;
+ const idx = row.fieldIndex;
+ const oldId = row.fileId;
+
+ try {
+ const uploaded = await postUpload(src, row.fileId);
+ const newId = uploaded.id;
+
+ const entry = await getEntryPlural(plural, docId);
+ if (!entry) throw new Error("entrée introuvable");
+
+ const multiple = row.fieldMultiple;
+ const currentIds = getMediaIdsFromField(entry, field, multiple);
+
+ if (multiple) {
+ if (idx < 0 || idx >= currentIds.length) {
+ throw new Error(`index ${idx} hors limites (ids actuels : ${currentIds.join(",")})`);
+ }
+ if (currentIds[idx] !== oldId) {
+ throw new Error(
+ `id à l’index ${idx} est ${currentIds[idx]}, attendu ${oldId} — arrêt pour éviter corruption`
+ );
+ }
+ const next = currentIds.slice();
+ next[idx] = newId;
+ await putEntry(plural, docId, { [field]: next });
+ } else {
+ const cur = currentIds[0];
+ if (cur != null && cur !== oldId) {
+ throw new Error(`champ simple : attendu ancien id ${oldId}, vu ${cur}`);
+ }
+ await putEntry(plural, docId, { [field]: newId });
+ }
+
+ console.log(`OK upload+remplace : ${oldId} → ${newId} (${plural}/${docId})`);
+
+ const canDeleteOld = refCount[oldId] === 1;
+ if (canDeleteOld && oldId !== newId) {
+ try {
+ await deleteFile(oldId);
+ console.log(` ancien fichier Strapi ${oldId} supprimé`);
+ } catch (de) {
+ console.warn(` suppression ancien échouée (non bloquant) : ${de.message}`);
+ }
+ }
+ } catch (e) {
+ console.error(`ÉCHEC fileId=${row.fileId} : ${e.message}`);
+ }
+ }
+
+ console.log("\nTerminé. Recharge les fiches dans l’admin et vide le cache navigateur.");
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/strapi_extraction/media-sync/README.md b/strapi_extraction/media-sync/README.md
new file mode 100644
index 0000000..1a00eef
--- /dev/null
+++ b/strapi_extraction/media-sync/README.md
@@ -0,0 +1,58 @@
+# Pipeline médias Strapi → dossiers par section → WebP → ré-upload optionnel
+
+**Répertoire de travail (lourd)** : `strapi_extraction/extract/media-sync-work/` — ignoré par Git (`/.gitignore`).
+
+## Prérequis
+
+- Node 18+
+- Dépendance `sharp` installée depuis la racine (`npm install` déjà fait si le dépôt à jour).
+
+## Variables optionnelles
+
+| Variable | Rôle |
+|----------|------|
+| `STRAPI_URL` | Origine Strapi sans `/api` (défaut depuis `NEXT_PUBLIC_API_URL` ou prod). |
+| `STRAPI_API_TOKEN` | **Seulement** pour `04-upload-replace.js --execute`. Jeton créé dans Strapi → Settings → API Tokens (droits lecture + Upload + mise à jour des CT concernés). Ne pas committer ce jeton. |
+
+Chemins `.env.local` à la racine et `cmsbackend/.env` sont chargés par les scripts (`config.js` ou `04`).
+
+## Chaîne normale
+
+```powershell
+# depuis la racine du repo J:\my-next-site
+
+npm run media:inventory # 01 — liste tous les médias utilisés → media-inventory.json
+
+npm run media:download # 02 — téléchargement physique par section sous downloaded/
+
+npm run media:webp # 03 — conversion/copies sous webp/
+
+npm run media:upload # 04 — dry-run uniquement (aucune mutation)
+
+# Mutation CMS (**attention**) après lecture ci‑dessous :
+
+$env:STRAPI_API_TOKEN=""
+node strapi_extraction/media-sync/04-upload-replace.js --execute
+```
+
+### Organisation des dossiers
+
+- **`downloaded/{section}/{slug-du-contenu}/{fileId}_{nom-fichier}`**
+ Sections : `portfolio`, `competences`, `home`, `realisation-ias`, `glossaire`.
+- **`webp/...`** même structure, avec `.webp` (ou `.svg` copié en clair).
+
+Les **fichiers média uniques** sont dédupliqués par `fileId` : une seule fois sur disque, plusieurs lignes dans l’inventaire peuvent référencer le même téléchargement.
+
+### Erreur « Cannot convert argument to a ByteString » (upload)
+
+Les noms de fichiers sur disque peuvent contenir des caractères Unicode (tirets fins, flèches dans des noms AI, etc.). Le `FormData` HTTP n’accepte qu’un nom de fichier **ASCII** dans l’en-tête multipart ; le script **`04-upload-replace.js`** renomme donc en interne chaque envoi en `upload-{fileId}.webp` (voir `asciiUploadName` dans le fichier). Relance `--execute` après mise à jour du script.
+
+### Avant le `--execute` sur la prod
+
+1. **Tester d’abord** contre une instance Strapi locale (ex. importer la base vers `cmsbackend`, `npm run develop`, puis `$env:STRAPI_URL="http://localhost:1337"` et un jeton local).
+2. **Sauvegarder** la base et `/public/uploads`.
+3. Lire le préambule dans `04-upload-replace.js` ; le script vérifie que l’ancien id correspond encore avant remplacement pour limiter les corruptions.
+
+## Si l’étape ré-upload vous suffit après conversion manuelle
+
+Vous pouvez aussi **importer uniquement les WebP** depuis l’admin Strapi puis réassigner les champs à la main ; ce dépôt ne vous oblige pas à utiliser `04`.
diff --git a/strapi_extraction/media-sync/config.js b/strapi_extraction/media-sync/config.js
new file mode 100644
index 0000000..c4e5968
--- /dev/null
+++ b/strapi_extraction/media-sync/config.js
@@ -0,0 +1,78 @@
+/**
+ * Configuration partagée — sync médias Strapi (téléchargement / WebP / ré-upload).
+ * Variables d'environnement (optionnelles) :
+ * STRAPI_URL — origine sans /api (ex. https://api.fernandgrascalvet.com ou http://localhost:1337)
+ * STRAPI_API_TOKEN — jeton API Strapi (requis pour 04-upload-replace.js --execute)
+ */
+require("dotenv").config({ path: require("path").join(__dirname, "../../.env.local") });
+require("dotenv").config({ path: require("path").join(__dirname, "../../cmsbackend/.env") });
+
+const path = require("path");
+
+const STRAPI_URL =
+ process.env.STRAPI_URL ||
+ process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") ||
+ "https://api.fernandgrascalvet.com";
+
+const API_BASE = `${STRAPI_URL}/api`;
+
+/** Sortie : mirrors extract/ existant (gitignored lourd) */
+const WORK_ROOT = path.join(__dirname, "../extract/media-sync-work");
+const DIR_DOWNLOADED = path.join(WORK_ROOT, "downloaded");
+const DIR_WEBP = path.join(WORK_ROOT, "webp");
+const FILE_INVENTORY = path.join(WORK_ROOT, "media-inventory.json");
+
+/**
+ * Content-types avec champs média — aligné sur cmsbackend/src/api (schema.json par type).
+ * section = sous-dossier humain (portfolio, competences, …)
+ */
+const COLLECTIONS = [
+ {
+ plural: "projects",
+ singular: "project",
+ ref: "api::project.project",
+ section: "portfolio",
+ fields: [{ name: "picture", multiple: true }],
+ },
+ {
+ plural: "competences",
+ singular: "competence",
+ ref: "api::competence.competence",
+ section: "competences",
+ fields: [{ name: "picture", multiple: true }],
+ },
+ {
+ plural: "homepages",
+ singular: "homepage",
+ ref: "api::homepage.homepage",
+ section: "home",
+ fields: [{ name: "photo", multiple: false }],
+ },
+ {
+ plural: "realisation-ias",
+ singular: "realisation-ia",
+ ref: "api::realisation-ia.realisation-ia",
+ section: "realisation-ias",
+ fields: [{ name: "picture", multiple: true }],
+ },
+ {
+ plural: "glossaires",
+ singular: "glossaire",
+ ref: "api::glossaire.glossaire",
+ section: "glossaire",
+ fields: [{ name: "images", multiple: true }],
+ },
+];
+
+const PAGE_SIZE = 100;
+
+module.exports = {
+ STRAPI_URL,
+ API_BASE,
+ WORK_ROOT,
+ DIR_DOWNLOADED,
+ DIR_WEBP,
+ FILE_INVENTORY,
+ COLLECTIONS,
+ PAGE_SIZE,
+};
diff --git a/strapi_extraction/media-sync/lib/collect-media.js b/strapi_extraction/media-sync/lib/collect-media.js
new file mode 100644
index 0000000..c7ba6f0
--- /dev/null
+++ b/strapi_extraction/media-sync/lib/collect-media.js
@@ -0,0 +1,41 @@
+/**
+ * Helpers — extraire les fichiers média des réponses Strapi v5 (structure plate).
+ */
+
+function isUploadedImage(obj) {
+ if (!obj || typeof obj !== "object" || typeof obj.url !== "string") return false;
+ if (!obj.url.includes("/uploads/")) return false;
+ if (obj.mime && !obj.mime.startsWith("image/")) return false;
+ return true;
+}
+
+/** Liste { file, index } pour un champ média Strapi */
+function normalizeField(fieldVal, multiple) {
+ if (!fieldVal) return [];
+ if (multiple) {
+ const arr = Array.isArray(fieldVal) ? fieldVal : [fieldVal];
+ return arr
+ .filter(isUploadedImage)
+ .map((file, index) => ({ file, index }));
+ }
+ return isUploadedImage(fieldVal)
+ ? [{ file: fieldVal, index: 0 }]
+ : [];
+}
+
+function safeSlug(entry) {
+ const s =
+ entry.slug ??
+ entry.documentId ??
+ (entry.id != null ? String(entry.id) : "unknown");
+ return String(s)
+ .replace(/[^\wÀ-ÖØ-öø-ÿ.-]+/gu, "_")
+ .slice(0, 96)
+ .replace(/^_|_$/g, "") || "entry";
+}
+
+module.exports = {
+ normalizeField,
+ safeSlug,
+ isUploadedImage,
+};