extract_picture

This commit is contained in:
Ladebeze66 2026-04-28 12:22:24 +02:00
parent 13b4b6971c
commit d423372251
12 changed files with 1417 additions and 108 deletions

3
.gitignore vendored
View File

@ -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/

View File

@ -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"
>
<ReactMarkdown>{richText}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{richText}</ReactMarkdown>
</div>
)}

View File

@ -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)

673
package-lock.json generated
View File

@ -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",

View File

@ -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"
}

View File

@ -0,0 +1,139 @@
/**
* 01 Appelle lAPI 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);
});

View File

@ -0,0 +1,108 @@
/**
* 02 Télécharge chaque fichier unique de linventaire 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 linventaire. Lance dabord : 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);
});

View File

@ -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);
});

View File

@ -0,0 +1,270 @@
/**
* 04 -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 lenv : 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 dinventaire 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+00U+FF
* (ex. U+2194 flèches dans des noms générés) provoquent lerreur 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 dactions 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 lancien id na quune référence dans linventaire\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 à lindex ${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 ladmin et vide le cache navigateur.");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -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 cidessous :
$env:STRAPI_API_TOKEN="<jeton>"
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 linventaire 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 naccepte quun nom de fichier **ASCII** dans len-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 dabord** 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 lancien 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 ladmin Strapi puis réassigner les champs à la main ; ce dépôt ne vous oblige pas à utiliser `04`.

View File

@ -0,0 +1,78 @@
/**
* Configuration partagée sync médias Strapi (téléchargement / WebP / -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,
};

View File

@ -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,
};