mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
Compare commits
No commits in common. "517eed915d447ab53c3528245a4bfd6839c012da" and "7efe114218efe9d514716c83468a9915644e1816" have entirely different histories.
517eed915d
...
7efe114218
3
.gitignore
vendored
3
.gitignore
vendored
@ -55,6 +55,3 @@ llm-api/*.pyc
|
|||||||
# Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25
|
# Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25
|
||||||
/chroma-index/
|
/chroma-index/
|
||||||
|
|
||||||
# Téléchargements + WebP générés par strapi_extraction/media-sync/ (poids important)
|
|
||||||
strapi_extraction/extract/media-sync-work/
|
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getApiUrl } from "../../utils/getApiUrl";
|
import { getApiUrl } from "../../utils/getApiUrl";
|
||||||
import { pickStrapiImage, type StrapiMediaLike } from "../../utils/strapiImage";
|
|
||||||
import VignetteCarousel from "../../components/VignetteCarousel";
|
import VignetteCarousel from "../../components/VignetteCarousel";
|
||||||
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
import ContentSectionCompetencesContainer from "../../components/ContentSectionCompetencesContainer";
|
||||||
|
|
||||||
@ -40,7 +38,7 @@ type Realisation = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
picture?: Array<StrapiMediaLike & { name?: string }>;
|
picture?: Array<{ url?: string; name?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Competence = {
|
type Competence = {
|
||||||
@ -202,17 +200,10 @@ export default function CompetencePage() {
|
|||||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
{realisations.map((realisation, idx) => {
|
{realisations.map((realisation, idx) => {
|
||||||
const pictures = realisation.picture ?? [];
|
const pictures = realisation.picture ?? [];
|
||||||
const images = pictures
|
const images = pictures.map((img) => ({
|
||||||
.map((img) => {
|
url: img?.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
|
||||||
const picked = pickStrapiImage(apiUrl, img, "card");
|
|
||||||
const url = picked?.src ?? (img?.url ? `${apiUrl}${img.url}` : null);
|
|
||||||
if (!url) return null;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
alt: img?.name || `Visuel de la réalisation ${realisation.name}`,
|
alt: img?.name || `Visuel de la réalisation ${realisation.name}`,
|
||||||
};
|
}));
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
const firstImage = images[0];
|
const firstImage = images[0];
|
||||||
|
|
||||||
// Comportement voulu : la vignette renvoie TOUJOURS vers la fiche
|
// Comportement voulu : la vignette renvoie TOUJOURS vers la fiche
|
||||||
@ -235,12 +226,11 @@ export default function CompetencePage() {
|
|||||||
{images.length > 1 ? (
|
{images.length > 1 ? (
|
||||||
<VignetteCarousel images={images} />
|
<VignetteCarousel images={images} />
|
||||||
) : firstImage ? (
|
) : firstImage ? (
|
||||||
<Image
|
<img
|
||||||
src={firstImage.url}
|
src={firstImage.url}
|
||||||
alt={firstImage.alt}
|
alt={firstImage.alt}
|
||||||
fill
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 42vw"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
||||||
|
|||||||
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import { pickStrapiImage } from "../utils/strapiImage";
|
|
||||||
import VignetteCarousel from "../components/VignetteCarousel";
|
import VignetteCarousel from "../components/VignetteCarousel";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import "../assets/main.css";
|
import "../assets/main.css";
|
||||||
@ -110,18 +108,10 @@ export default function Page() {
|
|||||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
{competences.map((competence, idx) => {
|
{competences.map((competence, idx) => {
|
||||||
const pictures = competence.picture ?? [];
|
const pictures = competence.picture ?? [];
|
||||||
const images = pictures
|
const images = pictures.map((img) => ({
|
||||||
.map((img) => {
|
url: img.url ? `${apiUrl}${img.url}` : "/placeholder.jpg",
|
||||||
const picked = pickStrapiImage(apiUrl, img, "card");
|
|
||||||
const url =
|
|
||||||
picked?.src ?? (img.url ? `${apiUrl}${img.url}` : null);
|
|
||||||
if (!url) return null;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
alt: img.name || `Visuel de la compétence ${competence.name}`,
|
alt: img.name || `Visuel de la compétence ${competence.name}`,
|
||||||
};
|
}));
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
const firstImage = images[0];
|
const firstImage = images[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -134,12 +124,11 @@ export default function Page() {
|
|||||||
{images.length > 1 ? (
|
{images.length > 1 ? (
|
||||||
<VignetteCarousel images={images} />
|
<VignetteCarousel images={images} />
|
||||||
) : firstImage ? (
|
) : firstImage ? (
|
||||||
<Image
|
<img
|
||||||
src={firstImage.url}
|
src={firstImage.url}
|
||||||
alt={firstImage.alt}
|
alt={firstImage.alt}
|
||||||
fill
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 42vw"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
@ -78,17 +77,13 @@ export default function Carousel({ images, className }: CarouselProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{images.map((img, index) => (
|
{images.map((img, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide key={index} className="flex h-full items-center justify-center">
|
||||||
key={index}
|
<img
|
||||||
className="relative flex h-full min-h-0 w-full items-center justify-center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={img.url}
|
src={img.url}
|
||||||
alt={img.alt}
|
alt={img.alt}
|
||||||
fill
|
className="h-full w-full cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
|
||||||
className="cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
|
|
||||||
sizes="(max-width: 768px) 100vw, min(48rem, 100vw)"
|
|
||||||
onClick={() => setSelectedImage(img.url)}
|
onClick={() => setSelectedImage(img.url)}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
@ -66,17 +65,13 @@ export default function CarouselCompetences({ images, className }: CarouselProps
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{images.map((img, index) => (
|
{images.map((img, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide key={index} className="flex h-full items-center justify-center">
|
||||||
key={index}
|
<img
|
||||||
className="relative flex h-full min-h-0 w-full items-center justify-center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={img.url}
|
src={img.url}
|
||||||
alt={img.alt}
|
alt={img.alt}
|
||||||
fill
|
className="h-full w-full cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
|
||||||
className="cursor-zoom-in object-cover transition-transform duration-300 hover:scale-[1.02]"
|
|
||||||
sizes="(max-width: 768px) 100vw, min(48rem, 100vw)"
|
|
||||||
onClick={() => setSelectedImage(img.url)}
|
onClick={() => setSelectedImage(img.url)}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -4,12 +4,16 @@ import Link from "next/link";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchData } from "../utils/fetchData";
|
import { fetchData } from "../utils/fetchData";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import { pickStrapiImage, type StrapiMediaLike } from "../utils/strapiImage";
|
|
||||||
import Carousel from "./Carousel";
|
import Carousel from "./Carousel";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
|
|
||||||
interface ImageData extends StrapiMediaLike {
|
interface ImageData {
|
||||||
|
url: string;
|
||||||
|
formats?: {
|
||||||
|
large?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,21 +156,10 @@ export default function ContentSection({
|
|||||||
const richText = data.Resum ?? data.resum ?? "";
|
const richText = data.Resum ?? data.resum ?? "";
|
||||||
|
|
||||||
const images =
|
const images =
|
||||||
picture
|
picture?.map((img: ImageData) => ({
|
||||||
?.map((img: ImageData) => {
|
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
||||||
const picked = pickStrapiImage(apiUrl, img, "full");
|
|
||||||
const url =
|
|
||||||
picked?.src ??
|
|
||||||
(img.url || img.formats?.large?.url
|
|
||||||
? `${apiUrl}${img.formats?.large?.url ?? img.url}`
|
|
||||||
: null);
|
|
||||||
if (!url) return null;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
alt: img.name || `Visuel du projet ${name}`,
|
alt: img.name || `Visuel du projet ${name}`,
|
||||||
};
|
})) || [];
|
||||||
})
|
|
||||||
.filter((item): item is { url: string; alt: string } => item != null) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-5 px-4 pb-10 sm:px-6">
|
||||||
@ -218,7 +211,7 @@ export default function ContentSection({
|
|||||||
prose-li:marker:text-primary
|
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"
|
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 remarkPlugins={[remarkGfm]}>{richText}</ReactMarkdown>
|
<ReactMarkdown>{richText}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,16 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import { pickStrapiImage, type StrapiMediaLike } from "../utils/strapiImage";
|
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import CarouselCompetences from "./CarouselCompetences";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import ModalGlossaire from "./ModalGlossaire";
|
import ModalGlossaire from "./ModalGlossaire";
|
||||||
|
|
||||||
interface ImageData extends StrapiMediaLike {
|
interface ImageData {
|
||||||
|
url: string;
|
||||||
|
formats?: {
|
||||||
|
large?: { url: string };
|
||||||
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,21 +132,10 @@ export default function ContentSectionCompetences({
|
|||||||
const { name, content, picture } = competenceData;
|
const { name, content, picture } = competenceData;
|
||||||
|
|
||||||
const images =
|
const images =
|
||||||
picture
|
picture?.map((img) => ({
|
||||||
?.map((img: ImageData) => {
|
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
||||||
const picked = pickStrapiImage(apiUrl, img, "full");
|
|
||||||
const url =
|
|
||||||
picked?.src ??
|
|
||||||
(img.url || img.formats?.large?.url
|
|
||||||
? `${apiUrl}${img.formats?.large?.url ?? img.url}`
|
|
||||||
: null);
|
|
||||||
if (!url) return null;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
alt: img.name || `Visuel de la compétence ${name}`,
|
alt: img.name || `Visuel de la compétence ${name}`,
|
||||||
};
|
})) || [];
|
||||||
})
|
|
||||||
.filter((item): item is { url: string; alt: string } => item != null) || [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforme le Markdown en injectant des spans `.glossary-keyword` / `.chatbot-keyword`
|
* Transforme le Markdown en injectant des spans `.glossary-keyword` / `.chatbot-keyword`
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import { useEffect } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import CarouselCompetences from "./CarouselCompetences";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import { pickStrapiImage, type StrapiMediaLike } from "../utils/strapiImage";
|
|
||||||
|
|
||||||
interface ImageData extends StrapiMediaLike {
|
interface ImageData {
|
||||||
|
url: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
formats?: {
|
||||||
|
large?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlossaireMot {
|
interface GlossaireMot {
|
||||||
@ -38,21 +43,10 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const images =
|
const images =
|
||||||
mot.images
|
mot.images?.map((img) => ({
|
||||||
?.map((img: ImageData) => {
|
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
||||||
const picked = pickStrapiImage(apiUrl, img, "full");
|
|
||||||
const url =
|
|
||||||
picked?.src ??
|
|
||||||
(img.url || img.formats?.large?.url
|
|
||||||
? `${apiUrl}${img.formats?.large?.url ?? img.url}`
|
|
||||||
: null);
|
|
||||||
if (!url) return null;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
alt: img.name || "Illustration",
|
alt: img.name || "Illustration",
|
||||||
};
|
})) || [];
|
||||||
})
|
|
||||||
.filter((item): item is { url: string; alt: string } => item != null) || [];
|
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Autoplay, Pagination } from "swiper/modules";
|
import { Autoplay, Pagination } from "swiper/modules";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
@ -55,16 +54,12 @@ export default function VignetteCarousel({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{images.map((img, index) => (
|
{images.map((img, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide key={index} className="flex h-full items-center justify-center">
|
||||||
key={index}
|
<img
|
||||||
className="relative flex h-full min-h-0 w-full items-center justify-center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={img.url}
|
src={img.url}
|
||||||
alt={img.alt}
|
alt={img.alt}
|
||||||
fill
|
className="h-full w-full object-cover"
|
||||||
className="object-cover"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 42vw"
|
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -89,12 +89,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
href="https://fonts.gstatic.com"
|
href="https://fonts.gstatic.com"
|
||||||
crossOrigin=""
|
crossOrigin=""
|
||||||
/>
|
/>
|
||||||
{/* API Strapi (médias JSON + images) — origine alignée sur NEXT_PUBLIC_API_URL. */}
|
|
||||||
<link
|
|
||||||
rel="preconnect"
|
|
||||||
href={process.env.NEXT_PUBLIC_API_URL || "https://api.fernandgrascalvet.com"}
|
|
||||||
crossOrigin=""
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
|
||||||
|
|||||||
18
app/page.tsx
18
app/page.tsx
@ -1,12 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import "./assets/main.css";
|
import "./assets/main.css";
|
||||||
import { getApiUrl } from "./utils/getApiUrl";
|
import { getApiUrl } from "./utils/getApiUrl";
|
||||||
import { pickStrapiImage } from "./utils/strapiImage";
|
|
||||||
|
|
||||||
async function getHomepageData() {
|
async function getHomepageData() {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
@ -108,12 +106,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const title = homepage.title ?? "Titre par défaut";
|
const title = homepage.title ?? "Titre par défaut";
|
||||||
const cv: string = homepage.cv ?? "";
|
const cv: string = homepage.cv ?? "";
|
||||||
const portraitPick = homepage.photo
|
const imageUrl = homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null;
|
||||||
? pickStrapiImage(apiUrl, homepage.photo, "hero")
|
|
||||||
: null;
|
|
||||||
const imageUrl =
|
|
||||||
portraitPick?.src ??
|
|
||||||
(homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-3 px-4 pb-10 sm:px-6">
|
<div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-3 px-4 pb-10 sm:px-6">
|
||||||
@ -127,14 +120,11 @@ export default function HomePage() {
|
|||||||
<div className="mx-auto md:mx-0">
|
<div className="mx-auto md:mx-0">
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<div className="rounded-sheet bg-primary p-1 shadow-ambient-sm">
|
<div className="rounded-sheet bg-primary p-1 shadow-ambient-sm">
|
||||||
<div className="relative h-48 w-48 overflow-hidden rounded-[1.25rem] sm:h-56 sm:w-56 md:h-64 md:w-64">
|
<div className="overflow-hidden rounded-[1.25rem]">
|
||||||
<Image
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={`Portrait de ${title}`}
|
alt={`Portrait de ${title}`}
|
||||||
fill
|
className="h-48 w-48 object-cover object-center sm:h-56 sm:w-56 md:h-64 md:w-64"
|
||||||
className="object-cover object-center"
|
|
||||||
sizes="(max-width: 768px) 12rem, 16rem"
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
import { pickStrapiImage } from "../utils/strapiImage";
|
|
||||||
import VignetteCarousel from "../components/VignetteCarousel";
|
import VignetteCarousel from "../components/VignetteCarousel";
|
||||||
import "../assets/main.css";
|
import "../assets/main.css";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
@ -115,16 +113,10 @@ export default function Page() {
|
|||||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-6">
|
||||||
{projects.map((project, idx) => {
|
{projects.map((project, idx) => {
|
||||||
const pictures = project.picture ?? [];
|
const pictures = project.picture ?? [];
|
||||||
const images = pictures
|
const images = pictures.map((img) => ({
|
||||||
.map((img) => {
|
url: `${apiUrl}${img.url}`,
|
||||||
const picked = pickStrapiImage(apiUrl, img, "card");
|
|
||||||
if (!picked) return null;
|
|
||||||
return {
|
|
||||||
url: picked.src,
|
|
||||||
alt: img.name || `Visuel du projet ${project.name}`,
|
alt: img.name || `Visuel du projet ${project.name}`,
|
||||||
};
|
}));
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
const firstImage = images[0];
|
const firstImage = images[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,12 +129,11 @@ export default function Page() {
|
|||||||
{images.length > 1 ? (
|
{images.length > 1 ? (
|
||||||
<VignetteCarousel images={images} />
|
<VignetteCarousel images={images} />
|
||||||
) : firstImage ? (
|
) : firstImage ? (
|
||||||
<Image
|
<img
|
||||||
src={firstImage.url}
|
src={firstImage.url}
|
||||||
alt={firstImage.alt}
|
alt={firstImage.alt}
|
||||||
fill
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||||
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 42vw"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
<div className="flex h-full w-full items-center justify-center text-sm text-on-surface-variant">
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sélection d'une variante Strapi (thumbnail / small / medium / large) pour
|
|
||||||
* limiter le poids transféré sans sacrifier la qualité affichée.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type StrapiImagePreset = "card" | "thumbnail" | "hero" | "full";
|
|
||||||
|
|
||||||
export interface StrapiFormatEntry {
|
|
||||||
url?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
height?: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StrapiMediaLike {
|
|
||||||
url?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
height?: number | null;
|
|
||||||
formats?: {
|
|
||||||
thumbnail?: StrapiFormatEntry | null;
|
|
||||||
small?: StrapiFormatEntry | null;
|
|
||||||
medium?: StrapiFormatEntry | null;
|
|
||||||
large?: StrapiFormatEntry | null;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function absUrl(apiUrl: string, path: string | null | undefined): string | null {
|
|
||||||
if (path == null || path === "") return null;
|
|
||||||
if (path.startsWith("http://") || path.startsWith("https://")) return path;
|
|
||||||
return `${apiUrl.replace(/\/$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Choisit l’URL + dimensions d’une image média Strapi selon le contexte d’affichage.
|
|
||||||
* Retourne null si aucune URL exploitable.
|
|
||||||
*/
|
|
||||||
export function pickStrapiImage(
|
|
||||||
apiUrl: string,
|
|
||||||
media: StrapiMediaLike | null | undefined,
|
|
||||||
preset: StrapiImagePreset
|
|
||||||
): { src: string; width: number; height: number } | null {
|
|
||||||
if (!media?.url) return null;
|
|
||||||
|
|
||||||
const f = media.formats;
|
|
||||||
let chosen: StrapiFormatEntry | undefined;
|
|
||||||
|
|
||||||
switch (preset) {
|
|
||||||
case "thumbnail":
|
|
||||||
chosen = f?.thumbnail ?? f?.small ?? undefined;
|
|
||||||
break;
|
|
||||||
case "card":
|
|
||||||
chosen = f?.medium ?? f?.small ?? f?.thumbnail ?? undefined;
|
|
||||||
break;
|
|
||||||
case "hero":
|
|
||||||
case "full":
|
|
||||||
chosen = f?.large ?? f?.medium ?? f?.small ?? undefined;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
chosen = f?.medium ?? f?.small;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relPath = chosen?.url ?? media.url;
|
|
||||||
const src = absUrl(apiUrl, relPath);
|
|
||||||
if (!src) return null;
|
|
||||||
|
|
||||||
const width = Number(chosen?.width ?? media.width) || 800;
|
|
||||||
const height = Number(chosen?.height ?? media.height) || 600;
|
|
||||||
|
|
||||||
return { src, width, height };
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Frontend Next.js
|
# Frontend Next.js
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@ -43,9 +43,7 @@
|
|||||||
## Configuration Next
|
## Configuration Next
|
||||||
|
|
||||||
- `next.config.ts` : `rewrites` de `/api/:path*` vers `${API_URL}/api/:path*` où `API_URL` vient de `NEXT_PUBLIC_API_URL` ou défaut production.
|
- `next.config.ts` : `rewrites` de `/api/:path*` vers `${API_URL}/api/:path*` où `API_URL` vient de `NEXT_PUBLIC_API_URL` ou défaut production.
|
||||||
- **`next/image`** : `images.remotePatterns` vers les chemins **`/uploads/**`** de l’API Strapi (HTTPS prod + `localhost` / `127.0.0.1:1337` en dev) ; `formats` AVIF/WebP ; **`compress: false`** (compatibilité reverse proxy IIS — voir [`09-performances-images.md`](./09-performances-images.md) §4.1).
|
- `images.domains` : `localhost`, `api.fernandgrascalvet.com`.
|
||||||
- **Médias Strapi** : utilitaire **`pickStrapiImage`** (`app/utils/strapiImage.ts`) pour préférer les variantes `medium` / `large` selon le contexte (liste, hero, galerie).
|
|
||||||
- Plan **Server Components** (données Strapi), non implémenté : [`10-plan-server-components.md`](./10-plan-server-components.md).
|
|
||||||
|
|
||||||
## Composants notables
|
## Composants notables
|
||||||
|
|
||||||
@ -61,7 +59,6 @@
|
|||||||
app/layout.tsx
|
app/layout.tsx
|
||||||
app/page.tsx
|
app/page.tsx
|
||||||
app/utils/getApiUrl.ts
|
app/utils/getApiUrl.ts
|
||||||
app/utils/strapiImage.ts
|
|
||||||
app/utils/fetchData.ts
|
app/utils/fetchData.ts
|
||||||
app/api/contact/route.ts
|
app/api/contact/route.ts
|
||||||
next.config.ts
|
next.config.ts
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Outils `strapi_extraction/`
|
# Outils `strapi_extraction/`
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28
|
**Dernière mise à jour :** 2026-04-22
|
||||||
|
|
||||||
Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les
|
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
|
données issues de l'API Strapi en base de connaissance chatbot (hors runtime
|
||||||
@ -84,18 +84,8 @@ comme source de vérité sans comparer au CMS.
|
|||||||
Ces points seront corrigés en même temps que l'enrichissement du vault
|
Ces points seront corrigés en même temps que l'enrichissement du vault
|
||||||
(glossaire + homepage Strapi → notes `40-Glossaire/` et `30-Parcours/`).
|
(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
|
## Liens complémentaires
|
||||||
|
|
||||||
- Vault + retrieval : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md)
|
- 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)
|
- 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)
|
- Schémas Strapi : [`03-cms-strapi.md`](./03-cms-strapi.md)
|
||||||
- Performances images (audit) : [`09-performances-images.md`](./09-performances-images.md)
|
|
||||||
|
|||||||
@ -1,363 +0,0 @@
|
|||||||
# Audit performances images & dev mode
|
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28 (révision compression IIS/Next + lien plan SC)
|
|
||||||
**Statut :** lots **A, B, C** et socle du lot **F** réalisés dans le code Next ; inventaire médias (Lot **D**) et autres lots **non** faits sauf mention.
|
|
||||||
|
|
||||||
> Document hybridé : conserve l’audit historique (§2 inventaire médias inchangé
|
|
||||||
> tant qu’on n’a pas re-mesuré `cmsbackend/public/uploads/`). Les §3–4 reflètent
|
|
||||||
> l’implémentation actuelle après l’itération perf front.
|
|
||||||
|
|
||||||
## 1. Contexte du problème
|
|
||||||
|
|
||||||
Sur certains navigateurs (notamment ceux qui ne sont pas Chromium ou en
|
|
||||||
connexion limitée), les pages `portfolio/`, `competences/` et la home mettent
|
|
||||||
plusieurs secondes à afficher leurs visuels. L'hypothèse de départ était :
|
|
||||||
|
|
||||||
- **« les images devraient déjà être en WebP »** ;
|
|
||||||
- **« Strapi en dev ralentit la livraison des images »** ;
|
|
||||||
- **« Next + Strapi tournent en `dev`, ce qui dégrade les perfs »**.
|
|
||||||
|
|
||||||
Le diagnostic ci-dessous valide partiellement ces hypothèses et en révèle
|
|
||||||
d'autres, plus structurelles, qui pèsent davantage que le mode `dev`.
|
|
||||||
|
|
||||||
## 2. Inventaire des médias Strapi (mesure)
|
|
||||||
|
|
||||||
Mesures faites sur `cmsbackend/public/uploads/` (28/04/2026).
|
|
||||||
|
|
||||||
### 2.1 Vue globale
|
|
||||||
|
|
||||||
| Catégorie | Fichiers | Poids |
|
|
||||||
|---|---:|---:|
|
|
||||||
| **Total uploads** | 2 603 | **1 034,6 MB** |
|
|
||||||
| Originaux (sans variantes responsive) | 523 | 569,6 MB |
|
|
||||||
| Variantes responsive Strapi (`thumbnail_/small_/medium_/large_`) | 2 079 | 465,0 MB |
|
|
||||||
|
|
||||||
### 2.2 Originaux par format
|
|
||||||
|
|
||||||
| Extension | Fichiers | Poids | Statut |
|
|
||||||
|---|---:|---:|---|
|
|
||||||
| `.webp` | 252 | 92,5 MB | ✅ converti |
|
|
||||||
| `.jpg` | 59 | 39,4 MB | ⚠️ à convertir |
|
|
||||||
| `.png` | 212 | **437,6 MB** | 🔴 à convertir en priorité |
|
|
||||||
|
|
||||||
**190 PNG dépassent 1 MB** (424 MB cumulés), avec un top 10 entre 3 et 4 MB
|
|
||||||
par fichier (ex. `vase_*.png` à 4,25 MB, illustrations Midjourney à 3,5 MB).
|
|
||||||
|
|
||||||
> **Constat n° 1 :** l'hypothèse « tout est déjà en WebP » est fausse. **271
|
|
||||||
> originaux non-WebP** représentent **84 %** du poids des originaux. La
|
|
||||||
> conversion ciblée des PNG > 1 MB suffirait à diviser le total par 3 ou 4.
|
|
||||||
|
|
||||||
### 2.3 Variantes responsive Strapi
|
|
||||||
|
|
||||||
| Variante | Fichiers | Poids |
|
|
||||||
|---|---:|---:|
|
|
||||||
| `large_*` | 518 | 242,9 MB |
|
|
||||||
| `medium_*` | 518 | 139,6 MB |
|
|
||||||
| `small_*` | 520 | 65,7 MB |
|
|
||||||
| `thumbnail_*` | 523 | 16,9 MB |
|
|
||||||
|
|
||||||
Strapi génère bien ces variantes — mais **dans le format de l'original**. Donc
|
|
||||||
un `.png` de 4 MB produit un `large_…png` de ~1 MB, alors qu'une version
|
|
||||||
WebP serait à ~150-300 KB pour la même qualité perçue.
|
|
||||||
|
|
||||||
## 3. Comment les images sont consommées par Next (état courant)
|
|
||||||
|
|
||||||
### 3.1 Utilitaire `pickStrapiImage` (`app/utils/strapiImage.ts`)
|
|
||||||
|
|
||||||
Toute lecture d’un média Strapi passant par le front doit préférer une **variante**
|
|
||||||
à l’original :
|
|
||||||
|
|
||||||
| Preset | Usage typique | Ordre de préférence |
|
|
||||||
|--------|----------------|---------------------|
|
|
||||||
| `card` | Grilles liste portfolio / compétences / vignettes `realisation-ia` | `medium` → `small` → `thumbnail` → original |
|
|
||||||
| `hero` | Portrait hero home | `large` → `medium` → `small` → original |
|
|
||||||
| `full` | Galeries fiche projet, compétence, glossaire (carousel détail) | `large` → `medium` → `small` → original |
|
|
||||||
|
|
||||||
Les pages et composants listés au §3.2 appellent cet utilitaire puis
|
|
||||||
construisent une URL absolue avec `getApiUrl()`. Si Strapi ne renvoie pas de
|
|
||||||
bloc `formats` (upload incomplet ou vieux contenu), on retombe sur
|
|
||||||
`formats.large ?? url` comme avant pour les zones **déjà** codées ainsi.
|
|
||||||
|
|
||||||
### 3.2 `next/image` + `sizes`
|
|
||||||
|
|
||||||
**Import** `next/image` utilisé pour les flux suivants :
|
|
||||||
|
|
||||||
- **`app/page.tsx`** — portrait hero : `fill` dans un bloc dimensionné +
|
|
||||||
**`priority`** (LCP).
|
|
||||||
- **`app/portfolio/page.jsx`**, **`app/competences/page.jsx`**,
|
|
||||||
**`app/competences/[slug]/page.tsx`** — vignettes : `fill` +
|
|
||||||
`sizes` adaptatif.
|
|
||||||
- **`VignetteCarousel.tsx`**, **`Carousel.tsx`**, **`CarouselCompetences.tsx`**
|
|
||||||
— slides Swiper en `fill` avec `sizes` ; la **lightbox** reste une **`<img>`**
|
|
||||||
native (zoom plein cadre sans contraintes de dimensions Next).
|
|
||||||
|
|
||||||
**Non couvert dans ce lot** : `placeholder="blur"` avec `blurDataURL` dérivé
|
|
||||||
de `formats.thumbnail` (restait dans le périmètre « idéal » du lot F
|
|
||||||
historique).
|
|
||||||
|
|
||||||
### 3.3 Configuration `next.config.ts`
|
|
||||||
|
|
||||||
- **`images.remotePatterns`** vers `uploads/**` pour
|
|
||||||
**`https://api.fernandgrascalvet.com`**, **`http://localhost:1337`** et
|
|
||||||
**`http://127.0.0.1:1337`** (aligné sur `getApiUrl()` en dev).
|
|
||||||
- **`images.formats`** : `image/avif`, `image/webp`.
|
|
||||||
- Ancienne clé **`images.domains`** : retirée (dépréciée depuis Next 14).
|
|
||||||
|
|
||||||
Référence code : fichier `next.config.ts` à la racine du dépôt Next.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ancien diagnostic (archivé pour mémoire)
|
|
||||||
|
|
||||||
Avant cette itération, les listes chargeaient surtout **`img.url`** (original)
|
|
||||||
sans variantes Strapi ni `next/image`. Les métriques de l’inventaire §2 restent
|
|
||||||
utiles tant que les **originaux** côté disque sont lourds (PNG) : même avec des
|
|
||||||
variantes bien choisies, Strapi régénère souvent ces variantes **dans le même
|
|
||||||
format** que l’original — une conversion fichier (Lot **E**) reste pertinente
|
|
||||||
pour réduire le stockage total.
|
|
||||||
|
|
||||||
### 3.4 Toutes les pages sont `"use client"` + `useEffect`-fetch
|
|
||||||
|
|
||||||
`app/page.tsx`, `app/portfolio/page.jsx`, `app/competences/page.jsx`,
|
|
||||||
`app/competences/[slug]/page.tsx` : toutes commencent par `"use client"` et
|
|
||||||
font `fetch()` côté navigateur dans `useEffect`. Conséquences :
|
|
||||||
|
|
||||||
1. **First paint sans données** → on voit toujours le spinner ou les
|
|
||||||
squelettes, même quand Strapi répond vite. Le ressenti « ça rame » vient
|
|
||||||
en partie de là, indépendamment du poids des images.
|
|
||||||
2. Le HTML initial **ne contient aucune `<img>`** → le navigateur ne peut pas
|
|
||||||
préfetcher les visuels pendant le parsing HTML.
|
|
||||||
3. Pas de cache Next (`fetch` côté client = cache navigateur uniquement,
|
|
||||||
pas le data cache de Next).
|
|
||||||
|
|
||||||
> **Constat n° 3 :** migrer ces pages en **Server Components** (fetch dans le
|
|
||||||
> composant async + `revalidate: 60`) résoudrait à la fois le ressenti de
|
|
||||||
> lenteur **et** la latence images, puisque les `<img>` seraient dans le HTML
|
|
||||||
> initial — le navigateur lance les requêtes images en parallèle du JS. Plan détaillé :
|
|
||||||
> [`10-plan-server-components.md`](./10-plan-server-components.md).
|
|
||||||
|
|
||||||
## 4. Mode `dev` : impact réel
|
|
||||||
|
|
||||||
L'utilisateur souhaite **rester en dev pour le moment** — c'est noté. Voici
|
|
||||||
ce que ça coûte vraiment, du plus marquant au moins marquant.
|
|
||||||
|
|
||||||
### 4.1 Compression HTTP (Next) et reverse proxy IIS
|
|
||||||
|
|
||||||
**Décision effective (diagnostic avril 2026, exposition HTTPS derrière IIS) :** **`compress: false`**
|
|
||||||
dans `next.config.ts` (état actuel du dépôt).
|
|
||||||
|
|
||||||
- Un passage à **`compress: true`** a provoqué des **HTTP 500** côté
|
|
||||||
**navigateur** alors que Next loguait **`GET / 200`** : la réponse **gzip**
|
|
||||||
générée par Next n'était **pas** correctement gérée par la chaîne
|
|
||||||
**IIS + URL Rewrite + ARR** vers `http://localhost:3000` (buffer / en-têtes /
|
|
||||||
double traitement).
|
|
||||||
- La **compression dynamique IIS**, une fois son module installé, **n’a pas été
|
|
||||||
retenue** comme substitution fiable sur ce périmètre non plus tant que la combinaison
|
|
||||||
tunnel + Next n’a pas été retestée de façon isolée.
|
|
||||||
|
|
||||||
> Tant que le site est exposé **derrière IIS de cette façon**, ne pas réactiver
|
|
||||||
> **`compress: true`** côté Next sans **test** sur **`https://fernandgrascalvet.com`**.
|
|
||||||
> Le léger surplus de transfert brut **localhost ⇄ IIS** est acceptable en dev/serveur.
|
|
||||||
|
|
||||||
### 4.1b IIS — compression (référence)
|
|
||||||
|
|
||||||
- **Compression statique** : peut rester active sur les sites IIS (fichiers
|
|
||||||
servis directement par IIS) ; hors scope du corps HTML proxifié vers Next.
|
|
||||||
- **Compression dynamique** : module séparé (rôle serveur Web) ; **ne remplace pas**
|
|
||||||
le problème **`Content-Encoding`** venant de Next si un jour on réactive gzip côté app.
|
|
||||||
|
|
||||||
Le mode `next dev` reste sans minification agressive du JS (voir §4.2).
|
|
||||||
|
|
||||||
### 4.2 `next dev --turbopack`
|
|
||||||
|
|
||||||
Coût : pas de minification, pas de tree-shaking, sourcemaps, modules
|
|
||||||
instrumentés HMR. **Impact net :** bundle JS ~3-5× plus lourd qu'en prod,
|
|
||||||
mais ça ne touche pas les images. Visible surtout au premier chargement de
|
|
||||||
l'app.
|
|
||||||
|
|
||||||
### 4.3 `strapi develop`
|
|
||||||
|
|
||||||
- watcher tsc + reload admin sur chaque écriture ;
|
|
||||||
- logs verbeux ;
|
|
||||||
- pas de cache compilé — chaque requête ré-exécute le pipeline middlewares
|
|
||||||
complet.
|
|
||||||
|
|
||||||
**Impact sur les images :** marginal (sharp redimensionne et met en cache
|
|
||||||
les variantes au premier upload, pas à chaque requête). Strapi dev est
|
|
||||||
**lent à démarrer**, pas lent à servir un fichier statique.
|
|
||||||
|
|
||||||
### 4.4 Aucun cache HTTP côté Strapi
|
|
||||||
|
|
||||||
Le middleware `strapi::public` sert `cmsbackend/public/uploads/` sans
|
|
||||||
`Cache-Control` long terme. Chaque revisite re-télécharge potentiellement
|
|
||||||
l'image (selon ETag). C'est aggravé en dev car les builds suivants
|
|
||||||
invalidant tout. **Voir `cmsbackend/config/middlewares.ts`** :
|
|
||||||
|
|
||||||
```19:26:cmsbackend/config/middlewares.ts
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'strapi::poweredBy',
|
|
||||||
'strapi::query',
|
|
||||||
'strapi::body',
|
|
||||||
'strapi::session',
|
|
||||||
'strapi::favicon',
|
|
||||||
'strapi::public',
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Constat n° 4 (révisé encore) :** on garde **`compress: false`** côté Next
|
|
||||||
> pour éviter les **500 IIS** derrière reverse proxy ; le surplus de transfert HTML/JSON non gzip
|
|
||||||
> entre IIS et localhost est préféré à une page publique cassée.
|
|
||||||
> Le reste du mode `dev` reste plus lourd qu’une **build prod** (bundle JS, pas de data cache
|
|
||||||
> Next sur les pages entièrement client).
|
|
||||||
|
|
||||||
## 5. Autres pistes détectées en cours d'audit
|
|
||||||
|
|
||||||
### 5.1 `app/assets/images/` pèse 992 MB dans le repo
|
|
||||||
|
|
||||||
```
|
|
||||||
339 fichiers — 992,4 MB
|
|
||||||
.png : 137 fichiers — 871,7 MB (médianes ~3-10 MB par fichier)
|
|
||||||
.webp: 168 fichiers — 73,9 MB
|
|
||||||
.jpg : 33 fichiers — 46,8 MB
|
|
||||||
```
|
|
||||||
|
|
||||||
Dossier hérité de l'époque où les images étaient bundlées dans le code
|
|
||||||
Next. Aujourd'hui les pages tirent depuis Strapi, donc **ces PNG sont
|
|
||||||
probablement morts** mais alourdissent : `git clone`, sauvegardes, indexation
|
|
||||||
IDE, et potentiellement `next build` s'ils sont importés depuis un fichier
|
|
||||||
encore référencé. À auditer (`grep` sur les imports) puis purger ou archiver.
|
|
||||||
|
|
||||||
### 5.2 Portrait hero et LCP
|
|
||||||
|
|
||||||
**Partiellement traité (2026-04-28) :** le portrait utilise **`next/image`**
|
|
||||||
avec **`priority`** dans `app/page.tsx` (pas de `rel=preload` séparé). Le fetch
|
|
||||||
home reste **client** (`useEffect`) — le gain LCP complet viendra surtout avec
|
|
||||||
le lot **G** (Server Components).
|
|
||||||
|
|
||||||
### 5.3 Preconnect vers l’API Strapi
|
|
||||||
|
|
||||||
**Fait (2026-04-28) :** `app/layout.tsx` injecte
|
|
||||||
`<link rel="preconnect" href={process.env.NEXT_PUBLIC_API_URL || URL prod par défaut} crossOrigin="" />`
|
|
||||||
pour l’origine API (médias + JSON). En local, si `.env` pointe vers
|
|
||||||
`http://localhost:1337`, le preconnect cible ce host.
|
|
||||||
|
|
||||||
### 5.4 Wallpaper OK, alternatives mortes
|
|
||||||
|
|
||||||
`app/assets/images/wallpapersite_resultat.webp` ≈ **620 KB** — déjà WebP,
|
|
||||||
correct pour un fond plein écran. Le `wallpapersite.png` à côté pèse
|
|
||||||
6,8 MB et n'est plus utilisé : à supprimer.
|
|
||||||
|
|
||||||
## 6. Plan d'action proposé (du plus rentable au plus structurel)
|
|
||||||
|
|
||||||
Tri par ratio gain / effort. À discuter avant exécution.
|
|
||||||
|
|
||||||
### Quick wins (≤ 1 h, gros gains)
|
|
||||||
|
|
||||||
- [x] **Lot A — Lire les variantes Strapi côté Next.**
|
|
||||||
Implémenté via **`pickStrapiImage`** (`app/utils/strapiImage.ts`) et branchements
|
|
||||||
listes + carousels + fiches (voir §3). **À re-mesurer** sur `/portfolio` et
|
|
||||||
`/competences` (Network → Img) après redémarrage des services.
|
|
||||||
- [ ] **Lot B — Compression Next (`compress`) — annulé dans cette forme.**
|
|
||||||
Tentative **`compress: true`** puis **rétablissement à `false`** : conflit avec
|
|
||||||
reverse proxy IIS (**500** public). Voir §4.1.
|
|
||||||
- [x] **Lot C — `preconnect` API Strapi.**
|
|
||||||
Lien dans `app/layout.tsx`, origine pilotée par **`NEXT_PUBLIC_API_URL`**
|
|
||||||
avec repli sur l’API de production.
|
|
||||||
|
|
||||||
### Lots moyens (1-3 h chacun)
|
|
||||||
|
|
||||||
- [ ] **Lot D — Script d'inventaire & conversion WebP.**
|
|
||||||
Nouveau script `strapi_extraction/audit-images.js` qui :
|
|
||||||
1. parcourt `cmsbackend/public/uploads/` ;
|
|
||||||
2. identifie les originaux non-WebP > seuil (1 MB par défaut) ;
|
|
||||||
3. classe par section (en croisant avec
|
|
||||||
`extract/raw/projects-raw.json`, `competences-raw.json`,
|
|
||||||
`homepages-raw.json`) → produit
|
|
||||||
`strapi_extraction/extract/images-by-section/<section>/<slug>.json` ;
|
|
||||||
4. exporte un rapport `images-audit.md` (top n par poids, par section,
|
|
||||||
fichiers orphelins).
|
|
||||||
**Pas de conversion automatique au premier passage** — juste l'inventaire,
|
|
||||||
pour que l'utilisateur puisse vérifier la classification avant action.
|
|
||||||
|
|
||||||
- [ ] **Lot E — Conversion + ré-upload contrôlé.**
|
|
||||||
Une fois l'inventaire validé : second script
|
|
||||||
`strapi_extraction/convert-and-reupload.js` qui :
|
|
||||||
1. convertit les fichiers ciblés via `sharp` (qualité 80, conserve les
|
|
||||||
dimensions) → écrit dans `extract/converted/` ;
|
|
||||||
2. ré-upload via l'API REST Strapi (`POST /api/upload`) avec
|
|
||||||
`ref`/`refId`/`field` pour rattacher à la bonne entité ;
|
|
||||||
3. supprime l'ancien original via `DELETE /upload/files/:id` après
|
|
||||||
vérification.
|
|
||||||
**Réversibilité :** le script garde les originaux dans `extract/backup/`
|
|
||||||
jusqu'à validation manuelle.
|
|
||||||
|
|
||||||
- [x] **Lot F — Migration `<img>` → `next/image` (socle).**
|
|
||||||
Fait pour les flux principaux (listes, carousels, hero) + **`remotePatterns`**
|
|
||||||
+ **`formats` AVIF/WebP**. **Non fait :** `deviceSizes` explicites (défaut
|
|
||||||
Next), **`placeholder="blur"`** — laissé en dette optionnelle.
|
|
||||||
|
|
||||||
### Lots structurels (½ journée +)
|
|
||||||
|
|
||||||
- [ ] **Lot G — Server Components pour `/portfolio`, `/competences` et `/`.**
|
|
||||||
Convertir les pages en composants async serveur, déplacer le `useEffect`
|
|
||||||
vers un sous-composant client uniquement pour l'interactivité (carousels,
|
|
||||||
modal). Bénéfices : HTML initial complet, fetch caché serveur, pas de
|
|
||||||
spinner au premier paint. Compatible avec Lot F (`next/image priority`).
|
|
||||||
**Plan rédigé :** [`10-plan-server-components.md`](./10-plan-server-components.md).
|
|
||||||
|
|
||||||
- [ ] **Lot H — Plugin Strapi pour conversion WebP à l'upload.**
|
|
||||||
Configurer `@strapi/provider-upload-local` (ou un plugin custom) pour
|
|
||||||
forcer la conversion WebP des nouveaux uploads via sharp. Évite la
|
|
||||||
ré-introduction de PNG bruts à l'avenir. **Hors scope du dev local
|
|
||||||
immédiat — à faire avant de remettre en prod.**
|
|
||||||
|
|
||||||
- [ ] **Lot I — Nettoyage `app/assets/images/`.**
|
|
||||||
Identifier les fichiers encore importés (`grep` sur les chemins).
|
|
||||||
Archiver (zip externe) le reste. Gain : ~900 MB sur le repo.
|
|
||||||
|
|
||||||
## 7. Métriques avant/après à capturer
|
|
||||||
|
|
||||||
À la fin de chaque lot, mesurer :
|
|
||||||
|
|
||||||
- Poids total transféré sur `/portfolio` (DevTools → Network → "Img"), en
|
|
||||||
cache vide et en cache plein ;
|
|
||||||
- LCP et CLS via Lighthouse en mode mobile/3G simulé ;
|
|
||||||
- Time-to-interactive sur Firefox + Safari (les navigateurs cibles
|
|
||||||
signalés comme problématiques).
|
|
||||||
|
|
||||||
Stocker les captures dans `docs-site-interne/captures/perf/` avec le
|
|
||||||
nom `<lot>-<avant|apres>.webp`.
|
|
||||||
|
|
||||||
## 8. Suite / dettes
|
|
||||||
|
|
||||||
- **Contenu Strapi** : harmonisation des médias et WebP côté CMS faite par l’auteur
|
|
||||||
— l’inventaire §2 n’a pas été **re-mesuré** ; relancer un passage sur
|
|
||||||
`cmsbackend/public/uploads/` si besoin de chiffres à jour.
|
|
||||||
- **Lot D** — script `strapi_extraction/audit-images.js` : **pas écrit**.
|
|
||||||
- **Lot E** — conversion + ré-upload : **hors code Next** ; voir pipeline
|
|
||||||
`strapi_extraction/media-sync` si utilisé pour le remplacement ciblé.
|
|
||||||
- **Lot G** — Server Components sur `/`, `/portfolio`, `/competences` : **à faire**
|
|
||||||
(voir [`10-plan-server-components.md`](./10-plan-server-components.md)).
|
|
||||||
- **Lots H / I** : inchangés (plugin Strapi upload, purge `app/assets/images/`).
|
|
||||||
|
|
||||||
Prochaine amélioration doc utile : captures **avant/après** réseau dans
|
|
||||||
`docs-site-interne/captures/perf/` une fois les services redémarrés et le
|
|
||||||
parcours manuel validé.
|
|
||||||
|
|
||||||
## 9. Liens internes
|
|
||||||
|
|
||||||
- Migration Server Components (plan) :
|
|
||||||
[`10-plan-server-components.md`](./10-plan-server-components.md)
|
|
||||||
- Pipeline d'extraction Strapi existant :
|
|
||||||
[`06-strapi-extraction.md`](./06-strapi-extraction.md)
|
|
||||||
- Architecture globale & ports :
|
|
||||||
[`01-architecture.md`](./01-architecture.md)
|
|
||||||
- Doc front Next :
|
|
||||||
[`02-frontend-next.md`](./02-frontend-next.md)
|
|
||||||
- État courant du chantier :
|
|
||||||
[`etat-actuel.md`](./etat-actuel.md)
|
|
||||||
- Roadmap :
|
|
||||||
[`feuille-de-route.md`](./feuille-de-route.md)
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
# Plan — migration vers Server Components (données Strapi)
|
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28
|
|
||||||
**Statut :** document de planification — **aucun changement de code appliqué** dans ce lot ; mise en œuvre quand vous le déciderez.
|
|
||||||
|
|
||||||
**Prérequis lu :** vous restez en **`npm run dev`** (Turbopack) pour le moment ; ce document décrit aussi ce que donne **`next build` / `next start`** en complément.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Objectif
|
|
||||||
|
|
||||||
Réduire le pattern actuel : pages marquées **`"use client"`** qui appellent **`fetch()` dans `useEffect`** après hydration.
|
|
||||||
|
|
||||||
Vers : **`async`** page (ou composant serveur parent) qui appelle **`fetch`** **côté serveur Next**, avec mise en **`cache`/revalidation** possible, puis rendu HTML **déjà rempli** de contenu texte + balises images.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. État actuel (référence doc + code)
|
|
||||||
|
|
||||||
| Page / zone | Fichier | Pattern actuel |
|
|
||||||
|-------------|---------|----------------|
|
|
||||||
| Accueil | `app/page.tsx` | `"use client"` + `useEffect` → `getHomepageData()` |
|
|
||||||
| Liste portfolio | `app/portfolio/page.jsx` | idem → `/api/projects` |
|
|
||||||
| Liste compétences | `app/competences/page.jsx` | idem → `/api/competences` |
|
|
||||||
| Compétence (vignettes réalisations) | `app/competences/[slug]/page.tsx` | idem, fetchs multiples |
|
|
||||||
|
|
||||||
Conséquences documentées dans [`09-performances-images.md`](./09-performances-images.md) §3.4 : premier paint sans données, pas de data cache Next sur ces flux, images découvertes tard.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Ce que « Server Components » implique (concrètement)
|
|
||||||
|
|
||||||
### 3.1 Principe
|
|
||||||
|
|
||||||
- Un **Server Component** est un composant **sans** `"use client"` qui peut être **`async`** et utiliser **`await fetch(...)`** directement dans le corps du composant.
|
|
||||||
- Le rendu s’exécute **sur le serveur Node** (processus Next), **une fois par requête** (sauf cache), pas dans le navigateur.
|
|
||||||
|
|
||||||
### 3.2 Ce qui reste **obligatoirement** en Client Components
|
|
||||||
|
|
||||||
Tout ce qui utilise des **hooks** React hors contexte « serveur » limité : **`useState`**, **`useEffect`**, **`useRef`** pour le menu mobile, événements clavier (**Escape**), **Swiper**, **portal** pour modales, **localStorage**, etc.
|
|
||||||
|
|
||||||
**Stratégie classique :** fichier page **sans** `"use client"` (serveur), qui **`await`** les données puis rend :
|
|
||||||
|
|
||||||
```txt
|
|
||||||
<>
|
|
||||||
<PageHeader … /> // peut être serveur
|
|
||||||
<ListeClientOuMixte … /> // sous-arbre `"use client"` uniquement où nécessaire
|
|
||||||
</>
|
|
||||||
```
|
|
||||||
|
|
||||||
Les **carousels** (`Carousel`, `VignetteCarousel`) restent très probablement en **clients** tant qu’ils dépendent de Swiper avec effets hydratés — on leur **passe en props** les URLs / textes déjà résolus côté serveur.
|
|
||||||
|
|
||||||
### 3.3 **Layout root** (`app/layout.tsx`)
|
|
||||||
|
|
||||||
Aujourd’hui : **`"use client"`** (+ menu burger, état drawer, etc.). **Option A :** garder le layout tel quel et ne migrer **que les pages feuilles** — Next autorise une page serveur même si le layout parent est client (limites : enfants peuvent être serveur sous certaines compositions ; selon Next 13–15 il faut vérifier qu’aucune contrainte n’« impose » tout client — en pratique souvent extraction **ServerLayout** léger OU **layouts par segment** `/portfolio/layout.tsx` serveur). **Option B (plus tard) :** scinder en **layout serveur** + **header/footer client** importés dynamiquement ou composants clients enfants.
|
|
||||||
|
|
||||||
C’est le point le plus **structurant** ; il sera arbitré au moment de l’implémentation (lot par lot).
|
|
||||||
|
|
||||||
### 3.4 Appels Strapi : URL
|
|
||||||
|
|
||||||
- **Côté serveur Next**, **`getApiUrl()`** sans `window` utilise **`NEXT_PUBLIC_API_URL`** (ou défaut prod). Pour le dev **sur la même machine**, s’assurer que cette env pointe soit vers **`http://localhost:1337`**, soit vers l’URL publique **si** IIS/Strapi le servent aussi — sinon risque que le SSR appelle **`https://api.fernand…`** depuis le serveur sans route réseau correcte (**variable d’env par environnement** recommandée).
|
|
||||||
- Harmoniser avec **rewrites** existants **`/api/*` → Strapi** : possibilité d’utiliser **`fetch(new URL('/api/...', request.url ?? base))`** en SSR si vous préférez passer par Next (même origin).
|
|
||||||
|
|
||||||
### 3.5 `fetch` et cache
|
|
||||||
|
|
||||||
- **`fetch(url, { next: { revalidate: 60 } })`** : ISR-like, données rafraîchies au plus toutes les **60** secondes selon docs Next 15 (`cache` par défaut a évolué — à relire [`next` doc](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching) au moment du codage).
|
|
||||||
- Pour du **toujours frais en dev**, **`cache: 'no-store'`** peut rester le comportement désiré jusqu’à validation.
|
|
||||||
|
|
||||||
### 3.6 Mode **`dev`** (Turbopack)
|
|
||||||
|
|
||||||
- Les Server Components **fonctionnent** en `next dev` ; le **cache** `fetch` est **moins représentatif** de la prod qu’avec `next start`.
|
|
||||||
- Le **gain perçu** (HTML complet, moins de waterfall client) reste **visible** en dev sur le **réseau** / **Elements** (contenu dans le HTML source).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Gains réels attendus
|
|
||||||
|
|
||||||
| Dimension | Avant (client fetch) | Après (données en serveur) |
|
|
||||||
|-----------|----------------------|----------------------------|
|
|
||||||
| **Premier contenu** | Spinner / squelette jusqu’à fin de `fetch` + JSON parse côté client | Texte + structure + **images** (`next/image` avec `src` déjà dans le HTML) plus tôt |
|
|
||||||
| **Waterfall** | HTML minimal → chargement JS → `useEffect` → fetch → re-render | Un aller-retour serveur Strapi lors du SSR Next (possible parallélisation `Promise.all`) |
|
|
||||||
| **Cache Next** | Aucun data cache pour ces pages | Possibilité **revalidate** / tags plus tard |
|
|
||||||
| **SEO / réseaux lents** | Contenu peu présent sans exécuter JS | Contenu lisible avec HTML seul (**meilleur** pour indexation ; utile même si votre besoin SEO est modeste) |
|
|
||||||
| **Lighthouse LCP** | Portrait / hero souvent retardé par la chaîne client | Déjà dans le flux initial SSR (**souvent gain LCP** mesurable après stabilisation)
|
|
||||||
|
|
||||||
**Limite honnête :** **`next dev`** conserve un JS **volumeux** (Turbopack, pas minifié comme prod). Le gain **bundle** sera **maximum** après **`next build` + `next start`** (ou équivalent).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Ordre de migration proposé (par risque croissant)
|
|
||||||
|
|
||||||
1. **`/portfolio`** (liste seule — une liste, pattern clair).
|
|
||||||
2. **`/competences`** (liste identique).
|
|
||||||
3. **`/`** (home — `getHomepageData` + retry ; extraire logique fetch dans `lib/` serveur).
|
|
||||||
4. **`/competences/[slug]`** (deux modes : vignettes réalisations vs redirect vers container compétences — attention aux branches).
|
|
||||||
|
|
||||||
Ensuite : fiches détail **`ContentSection`** / **`fetchData`** peuvent suivre une feuille de route analogue (nombreuses pages déjà partiellement factorisées via `fetchData`).
|
|
||||||
|
|
||||||
À chaque étape : **tests manuels** des routes, erreurs réseau Strapi, **images** encore correctes (**`pickStrapiImage`** inchangé côté props).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Hors scope immédiat (rappels)
|
|
||||||
|
|
||||||
- **Ne pas** renommer tous les fichiers **`.jsx`→`.tsx`** en même temps sans besoin ; migrer fichier par fichier.
|
|
||||||
- **Ne pas** activer **`compress: true`** côté Next tant que derrière **ARR/IIS** le tunnel reste incompatible (voir [`09-performances-images.md`](./09-performances-images.md) §4.6).
|
|
||||||
- **Évaluation `next build`** : à planifier après stabilisation SSR en dev ou sur une branche.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Liens internes
|
|
||||||
|
|
||||||
- Audit perf images & IIS : [`09-performances-images.md`](./09-performances-images.md)
|
|
||||||
- Front Next synthèse : [`02-frontend-next.md`](./02-frontend-next.md)
|
|
||||||
- Feuille de route : [`feuille-de-route.md`](./feuille-de-route.md)
|
|
||||||
- État actuel stack : [`etat-actuel.md`](./etat-actuel.md)
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# État actuel du site
|
# État actuel du site
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28 (perf images, compression IIS/Next, plan Server Components)
|
**Dernière mise à jour :** 2026-04-24 (doc compétences / realisation-ia + CONFIGURATION)
|
||||||
|
|
||||||
## Ce qui est en place
|
## Ce qui est en place
|
||||||
|
|
||||||
@ -13,7 +13,6 @@
|
|||||||
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger l’API après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
|
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger l’API après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
|
||||||
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
|
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
|
||||||
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
|
||||||
- **Performances images (front)** : utilitaire **`pickStrapiImage`**, variantes Strapi, **`next/image`** + `remotePatterns`, portrait **`priority`**, **`preconnect`** API dans `layout.tsx`. **`compress: false`** dans `next.config.ts` (obligatoire avec le reverse proxy IIS actuel — **`compress: true`** provoquait des **500** publics). Détail : [`09-performances-images.md`](./09-performances-images.md). Prochaine étape planifiée (sans implémentation engageant pour l’instant) : **Server Components** — [`10-plan-server-components.md`](./10-plan-server-components.md).
|
|
||||||
- Documentation opérationnelle : [`CONFIGURATION_SITE.md`](../CONFIGURATION_SITE.md) à la racine du dépôt (incl. ordre des compétences et routes dédiées, renvoi vers [02-frontend-next.md](./02-frontend-next.md)).
|
- Documentation opérationnelle : [`CONFIGURATION_SITE.md`](../CONFIGURATION_SITE.md) à la racine du dépôt (incl. ordre des compétences et routes dédiées, renvoi vers [02-frontend-next.md](./02-frontend-next.md)).
|
||||||
- **Captures d'écran** de référence (WebP) : [captures/](./captures/) — voir [INDEX.md](./captures/INDEX.md).
|
- **Captures d'écran** de référence (WebP) : [captures/](./captures/) — voir [INDEX.md](./captures/INDEX.md).
|
||||||
- **Décision produit** : une **rubrique homelab / serveur** (souvent évoquée en « phase 3 ») n’est **pas retenue** — pas d’évolution planifiée sur ce thème ; le parcours public reste portfolio, compétences (dont IA + réalisations) et contact.
|
- **Décision produit** : une **rubrique homelab / serveur** (souvent évoquée en « phase 3 ») n’est **pas retenue** — pas d’évolution planifiée sur ce thème ; le parcours public reste portfolio, compétences (dont IA + réalisations) et contact.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Feuille de route
|
# Feuille de route
|
||||||
|
|
||||||
**Dernière mise à jour :** 2026-04-28
|
**Dernière mise à jour :** 2026-04-24
|
||||||
|
|
||||||
Document vivant : ajuster les statuts et dates au fil du travail.
|
Document vivant : ajuster les statuts et dates au fil du travail.
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-7 (tokens + garde-fou + migration typo globale + layout racine + home + listes portfolio/compétences + fiches détail & GrasBot) faites le 2026-04-22. Reste l'étape 8 (contact + footer éditorial). |
|
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-7 (tokens + garde-fou + migration typo globale + layout racine + home + listes portfolio/compétences + fiches détail & GrasBot) faites le 2026-04-22. Reste l'étape 8 (contact + footer éditorial). |
|
||||||
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
||||||
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
|
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
|
||||||
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Lié au plan [`10-plan-server-components.md`](./10-plan-server-components.md) — extraction header/footer client possible |
|
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer |
|
||||||
| R5 | Chatbot GrasBot — retrieval local (Qwen3 + vault Obsidian) | v3 en place | Pipeline **graph + BM25** (`llm-api/search.py`), plus de RAG vectoriel ni de dépendance ChromaDB. Vault `vault-grasbot/` enrichi automatiquement (aliases/answers/priority). Lecture directe depuis `build-vault.py`, plus d'étape d'indexation. Doc : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md). Reste : fix `clean-api-data.js` (homepages + glossaires), affichage sources côté front, badge `grounded`, historique conversationnel, streaming. |
|
| R5 | Chatbot GrasBot — retrieval local (Qwen3 + vault Obsidian) | v3 en place | Pipeline **graph + BM25** (`llm-api/search.py`), plus de RAG vectoriel ni de dépendance ChromaDB. Vault `vault-grasbot/` enrichi automatiquement (aliases/answers/priority). Lecture directe depuis `build-vault.py`, plus d'étape d'indexation. Doc : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md). Reste : fix `clean-api-data.js` (homepages + glossaires), affichage sources côté front, badge `grounded`, historique conversationnel, streaming. |
|
||||||
|
|
||||||
## Moyen terme
|
## Moyen terme
|
||||||
@ -20,7 +20,7 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
|----|--------|--------|--------|
|
|----|--------|--------|--------|
|
||||||
| M1 | Tests (e2e ou smoke sur routes critiques) | À faire | |
|
| M1 | Tests (e2e ou smoke sur routes critiques) | À faire | |
|
||||||
| M2 | Accessibilité (navigation, contrastes, focus) | À faire | |
|
| M2 | Accessibilité (navigation, contrastes, focus) | À faire | |
|
||||||
| M3 | Performance (images Next/Image, bundle, SSR données) | En cours | Images + `pickStrapiImage` + preconnect : [`09-performances-images.md`](./09-performances-images.md). **`compress`** : maintenu à **`false`** (IIS). **Prochaine étape** : Server Components — plan [`10-plan-server-components.md`](./10-plan-server-components.md) (pas d’implémentation sans go utilisateur). |
|
| M3 | Performance (images Next/Image, bundle) | À faire | |
|
||||||
|
|
||||||
## Long terme / idées
|
## Long terme / idées
|
||||||
|
|
||||||
@ -59,5 +59,3 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()` — `num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault — nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 → 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` — nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
|
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()` — `num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault — nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 → 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` — nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
|
||||||
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++` → `cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md` → `grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
|
||||||
| 2026-04-24 | **Doc + configuration** : routes `/competences/[slug]/[realisation]`, entité Strapi `realisation-ia`, tri `order` et comportement vignettes/richtext documentés dans `02-frontend-next.md`, `04-api-llm-et-chatbot.md` (parcours public), `etat-actuel.md` ; `CONFIGURATION_SITE.md` : section *Contenu : compétences, réalisations IA et ordre d’affichage*. Décision : **pas de « phase 3 » homelab** sur le site (consignée dans l’état actuel). |
|
| 2026-04-24 | **Doc + configuration** : routes `/competences/[slug]/[realisation]`, entité Strapi `realisation-ia`, tri `order` et comportement vignettes/richtext documentés dans `02-frontend-next.md`, `04-api-llm-et-chatbot.md` (parcours public), `etat-actuel.md` ; `CONFIGURATION_SITE.md` : section *Contenu : compétences, réalisations IA et ordre d’affichage*. Décision : **pas de « phase 3 » homelab** sur le site (consignée dans l’état actuel). |
|
||||||
| 2026-04-28 | **Perf images Next** : lots A/B/C + socle F documentés dans [`09-performances-images.md`](./09-performances-images.md) — `pickStrapiImage`, `next/image` (listes, carousels, hero), `remotePatterns`, preconnect API. Lot B (`compress: true`) **annulé** : conflit IIS reverse proxy (500). |
|
|
||||||
| 2026-04-28 | **Plan Server Components** : nouveau doc [`10-plan-server-components.md`](./10-plan-server-components.md) (périmètre, gains, ordre de migration, contraintes `dev` / layout). **Aucun code** modifié dans ce lot. |
|
|
||||||
|
|||||||
@ -22,26 +22,7 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
images: {
|
images: {
|
||||||
formats: ["image/avif", "image/webp"],
|
domains: ["localhost", "api.fernandgrascalvet.com"],
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "api.fernandgrascalvet.com",
|
|
||||||
pathname: "/uploads/**",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "http",
|
|
||||||
hostname: "localhost",
|
|
||||||
port: "1337",
|
|
||||||
pathname: "/uploads/**",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "http",
|
|
||||||
hostname: "127.0.0.1",
|
|
||||||
port: "1337",
|
|
||||||
pathname: "/uploads/**",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
673
package-lock.json
generated
673
package-lock.json
generated
@ -30,7 +30,6 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@ -48,9 +47,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.10.0",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -58,9 +57,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.1.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -68,13 +67,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -87,17 +85,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-x64": {
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -110,17 +107,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -131,13 +127,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -148,13 +143,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
"version": "1.0.5",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -165,13 +159,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -214,13 +207,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -231,13 +223,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -248,13 +239,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -265,13 +255,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -282,13 +271,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -301,17 +289,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm64": {
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -324,7 +311,7 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-ppc64": {
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
@ -372,13 +359,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-s390x": {
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -391,17 +377,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -414,17 +399,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -437,17 +421,16 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -460,21 +443,20 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-wasm32": {
|
"node_modules/@img/sharp-wasm32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/runtime": "^1.2.0"
|
"@emnapi/runtime": "^1.7.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@ -503,13 +485,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-win32-ia32": {
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -523,13 +504,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -1184,20 +1164,6 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -1216,17 +1182,6 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@ -1321,8 +1276,8 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1905,13 +1860,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@ -3110,367 +3058,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@ -3499,51 +3086,6 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
@ -4171,8 +3713,8 @@
|
|||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
@ -4181,16 +3723,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"@img/colour": "^1.0.0",
|
||||||
"detect-libc": "^2.0.3",
|
"detect-libc": "^2.1.2",
|
||||||
"semver": "^7.6.3"
|
"semver": "^7.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@ -4199,25 +3741,30 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-darwin-arm64": "0.33.5",
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
"@img/sharp-darwin-x64": "0.33.5",
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
"@img/sharp-linux-arm": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
"@img/sharp-linux-arm64": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
"@img/sharp-linux-s390x": "0.33.5",
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
"@img/sharp-linux-x64": "0.33.5",
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
"@img/sharp-wasm32": "0.33.5",
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
"@img/sharp-win32-ia32": "0.33.5",
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
"@img/sharp-win32-x64": "0.33.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/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
@ -4325,16 +3872,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@ -6,11 +6,7 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"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": {
|
"dependencies": {
|
||||||
"@strapi/blocks-react-renderer": "^1.0.1",
|
"@strapi/blocks-react-renderer": "^1.0.1",
|
||||||
@ -35,7 +31,6 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,139 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
@ -1,270 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
# 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="<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 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`.
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@ -22,6 +22,5 @@
|
|||||||
</rewrite>
|
</rewrite>
|
||||||
<directoryBrowse enabled="true" />
|
<directoryBrowse enabled="true" />
|
||||||
<caching enableKernelCache="true" />
|
<caching enableKernelCache="true" />
|
||||||
<urlCompression doStaticCompression="true" doDynamicCompression="false" />
|
|
||||||
</system.webServer>
|
</system.webServer>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user