integration SEORank

This commit is contained in:
sebvtl728
2025-10-26 01:32:28 +02:00
parent 718eb8353b
commit e7e5c5e3fd
4 changed files with 810 additions and 379 deletions
@@ -1,7 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { updateHomePageACF, uploadImage } from "../../wordpress"; import { updateHomePageACF, updateRankMathMeta, uploadImage } from "../../wordpress";
import { getToken } from "../../auth"; import { getToken } from "../../auth";
import api from "../../api";
import { import {
Typography, Typography,
TextField, TextField,
@@ -10,10 +11,14 @@ import {
Paper, Paper,
Box, Box,
CircularProgress, CircularProgress,
Stack,
} from "@mui/material"; } from "@mui/material";
import ReactQuill from "react-quill"; import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css"; import "react-quill/dist/quill.snow.css";
const HOMEPAGE_ID = 13;
const HOMEPAGE_URL = "https://preprod.octopusdesign.fr/api-octopus/server/";
const GestionPageAccueil = () => { const GestionPageAccueil = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [heroTitle, setHeroTitle] = useState(""); const [heroTitle, setHeroTitle] = useState("");
@@ -39,6 +44,132 @@ const GestionPageAccueil = () => {
const [expertise3Title, setexpertise3Title] = useState(""); const [expertise3Title, setexpertise3Title] = useState("");
const [expertise3Text, setexpertise3Text] = useState(""); const [expertise3Text, setexpertise3Text] = useState("");
const [seoTitle, setSeoTitle] = useState("");
const [seoDescription, setSeoDescription] = useState("");
const [seoCanonical, setSeoCanonical] = useState("");
const [seoOgTitle, setSeoOgTitle] = useState("");
const [seoOgDescription, setSeoOgDescription] = useState("");
const [seoOgImage, setSeoOgImage] = useState("");
const [seoRobots, setSeoRobots] = useState("");
const [seoLoading, setSeoLoading] = useState(true);
const [seoError, setSeoError] = useState(null);
const glassCardSx = {
background: "rgba(7, 13, 28, 0.82)",
border: "1px solid rgba(132, 169, 255, 0.18)",
borderRadius: 4,
p: { xs: 2.5, md: 4 },
boxShadow: "0 35px 55px rgba(4, 6, 14, 0.65)",
backdropFilter: "blur(18px)",
color: "#f4f7ff",
};
const sectionTitleSx = {
fontWeight: "bold",
fontSize: { xs: "1.3rem", md: "1.6rem" },
mb: 2,
color: "#f6f8ff",
};
const primaryButtonSx = {
py: 1.2,
borderRadius: 999,
fontWeight: "bold",
textTransform: "none",
letterSpacing: 0.3,
background: "linear-gradient(135deg, #1ec6ff, #7f61ff)",
boxShadow: "0 15px 30px rgba(20, 36, 70, 0.35)",
"&:hover": {
background: "linear-gradient(135deg, #27b2ff, #5c4bff)",
boxShadow: "0 18px 35px rgba(17, 23, 46, 0.45)",
},
};
const labelSx = {
color: "rgba(224, 232, 255, 0.85)",
"&.Mui-focused": { color: "#8fd1ff" },
};
const textFieldSx = {
"& .MuiOutlinedInput-root": {
backgroundColor: "rgba(2, 5, 16, 0.75)",
color: "#f7f9ff",
borderRadius: 3,
"& fieldset": { borderColor: "rgba(255,255,255,0.15)" },
"&:hover fieldset": { borderColor: "#6bc9ff" },
"&.Mui-focused fieldset": { borderColor: "#2ee1ff" },
},
"& .MuiOutlinedInput-input": {
color: "#f7f9ff",
},
"& .MuiInputLabel-root": { color: "rgba(224,232,255,0.85)" },
};
const quillStyle = {
marginBottom: "20px",
backgroundColor: "rgba(255,255,255,0.98)",
borderRadius: "14px",
color: "#07122a",
};
const ghostButtonSx = {
py: 1.1,
borderRadius: 999,
fontWeight: "bold",
textTransform: "none",
borderColor: "rgba(255,255,255,0.4)",
color: "#e3e8ff",
"&:hover": {
borderColor: "rgba(255,255,255,0.7)",
background: "rgba(255,255,255,0.05)",
},
};
const loadSeoMeta = useCallback(async () => {
try {
setSeoLoading(true);
setSeoError(null);
const { data } = await api.get("rankmath/v1/getHead", {
params: { url: HOMEPAGE_URL },
});
if (
data?.success &&
data.head &&
typeof window !== "undefined" &&
window.DOMParser
) {
const parser = new window.DOMParser();
const parsedDocument = parser.parseFromString(
`<!doctype html><html><head>${data.head}</head><body></body></html>`,
"text/html"
);
const head = parsedDocument.head;
const pick = (selector, attribute = "content") =>
head.querySelector(selector)?.getAttribute(attribute) || "";
setSeoTitle(head.querySelector("title")?.textContent || "");
setSeoDescription(pick('meta[name="description"]'));
setSeoCanonical(
head.querySelector('link[rel="canonical"]')?.getAttribute("href") ||
pick('meta[property="og:url"]')
);
setSeoOgTitle(pick('meta[property="og:title"]'));
setSeoOgDescription(pick('meta[property="og:description"]'));
setSeoOgImage(pick('meta[property="og:image"]'));
setSeoRobots(pick('meta[name="robots"]'));
} else {
throw new Error("Réponse Rank Math invalide");
}
} catch (error) {
console.error("❌ Erreur chargement SEO Rank Math :", error);
setSeoError("Impossible de charger les métadonnées SEO.");
} finally {
setSeoLoading(false);
}
}, []);
// ✅ Redirection si non connecté // ✅ Redirection si non connecté
useEffect(() => { useEffect(() => {
if (!getToken()) { if (!getToken()) {
@@ -51,7 +182,7 @@ const GestionPageAccueil = () => {
const fetchPageData = async () => { const fetchPageData = async () => {
try { try {
const response = await fetch( const response = await fetch(
"https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/13?_fields=acf", `https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${HOMEPAGE_ID}?_fields=acf`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -106,11 +237,15 @@ const GestionPageAccueil = () => {
}; };
fetchPageData(); fetchPageData();
}, []); loadSeoMeta();
}, [loadSeoMeta]);
// ✅ Enregistrement ACF // ✅ Enregistrement ACF + Rank Math
const handleSave = async () => { const handleSave = async () => {
try { try {
setError(null);
setSuccessMessage("");
const newData = { const newData = {
hero_title: heroTitle, hero_title: heroTitle,
hero_text: heroText, hero_text: heroText,
@@ -134,13 +269,29 @@ const GestionPageAccueil = () => {
}, },
}; };
await updateHomePageACF(13, newData); await updateHomePageACF(HOMEPAGE_ID, newData);
await updateRankMathMeta(HOMEPAGE_ID, {
rank_math_title: seoTitle,
rank_math_description: seoDescription,
rank_math_canonical_url: seoCanonical || HOMEPAGE_URL,
rank_math_facebook_title: seoOgTitle || seoTitle,
rank_math_facebook_description: seoOgDescription || seoDescription,
rank_math_facebook_image: seoOgImage,
rank_math_twitter_title: seoOgTitle || seoTitle,
rank_math_twitter_description: seoOgDescription || seoDescription,
rank_math_twitter_image: seoOgImage,
rank_math_robots: seoRobots,
});
localStorage.setItem("missions", JSON.stringify(missions)); localStorage.setItem("missions", JSON.stringify(missions));
setSuccessMessage("✅ Modifications enregistrées avec succès !"); await loadSeoMeta();
setSuccessMessage("✅ Contenu & métadonnées enregistrés !");
setTimeout(() => setSuccessMessage(""), 3000); setTimeout(() => setSuccessMessage(""), 3000);
} catch (error) { } catch (error) {
console.error("❌ Erreur mise à jour :", error); console.error("❌ Erreur mise à jour :", error);
setError("⚠ Impossible de mettre à jour les champs ACF."); setError("⚠ Impossible de mettre à jour les champs ACF/SEO.");
} }
}; };
@@ -151,7 +302,7 @@ const GestionPageAccueil = () => {
try { try {
const imageId = await uploadImage(file); const imageId = await uploadImage(file);
setHeroImage(URL.createObjectURL(file)); setHeroImage(URL.createObjectURL(file));
await updateHomePageACF(13, { img_hero: imageId }); await updateHomePageACF(HOMEPAGE_ID, { img_hero: imageId });
setSuccessMessage("✅ Image mise à jour !"); setSuccessMessage("✅ Image mise à jour !");
} catch (error) { } catch (error) {
console.error("❌ Erreur upload image :", error); console.error("❌ Erreur upload image :", error);
@@ -181,25 +332,60 @@ const GestionPageAccueil = () => {
}; };
return ( return (
<Container maxWidth="md" sx={{ mt: 16, mb: 5 }}> <Box
<Paper
elevation={6}
sx={{ padding: 4, borderRadius: 3, backgroundColor: "#f8f9fa" }}
>
<Typography
variant="h4"
sx={{ sx={{
fontWeight: "bold", minHeight: "100vh",
textAlign: "center", py: { xs: 4, md: 8 },
mb: 3, px: { xs: 2, md: 4 },
color: "#0e467f", background:
"radial-gradient(circle at 10% 20%, #081736, #020611 55%, #01040b)",
position: "relative",
overflow: "hidden",
}} }}
> >
🏠 Gestion de la Page d'Accueil <Box
sx={{
position: "absolute",
inset: 0,
background:
"radial-gradient(circle at 70% 20%, rgba(49,209,255,0.25), transparent 45%), radial-gradient(circle at 20% 80%, rgba(125,87,255,0.25), transparent 40%)",
filter: "blur(40px)",
}}
/>
<Container maxWidth="lg" sx={{ position: "relative", zIndex: 1 }}>
<Paper
elevation={0}
sx={{
...glassCardSx,
textAlign: "center",
mb: 5,
}}
>
<Typography
variant="h3"
sx={{
fontWeight: 700,
mb: 1,
background: "linear-gradient(120deg,#ffffff,#59c2ff,#8f7bff)",
WebkitBackgroundClip: "text",
color: "transparent",
}}
>
🐙 Gestion de la Page d'Accueil
</Typography> </Typography>
<Typography sx={{ color: "rgba(255,255,255,0.7)" }}>
Administrez vos contenus, images et métadonnées SEO depuis une
interface moderne inspirée du glassmorphisme.
</Typography>
</Paper>
{loading ? ( {loading ? (
<Box display="flex" justifyContent="center" alignItems="center"> <Box
display="flex"
justifyContent="center"
alignItems="center"
py={6}
>
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : error ? ( ) : error ? (
@@ -207,84 +393,123 @@ const GestionPageAccueil = () => {
{error} {error}
</Typography> </Typography>
) : ( ) : (
<> <Stack spacing={4}>
{successMessage && ( {successMessage && (
<Box <Paper
elevation={0}
sx={{ sx={{
backgroundColor: "#d4edda", ...glassCardSx,
color: "#155724", borderColor: "rgba(72, 255, 184, 0.6)",
padding: "12px", background:
borderRadius: "8px", "linear-gradient(135deg, rgba(48,199,135,0.25), rgba(43,170,241,0.25))",
marginBottom: "16px",
textAlign: "center", textAlign: "center",
boxShadow: "0px 2px 10px rgba(0,0,0,0.1)",
}} }}
> >
<Typography variant="body1" fontWeight="bold"> <Typography fontWeight="bold" color="#e5fff5">
{successMessage} {successMessage}
</Typography> </Typography>
</Box> </Paper>
)} )}
{heroImage && ( <Stack
<Box sx={{ textAlign: "center", mb: 2 }}> direction={{ xs: "column", lg: "row" }}
<img spacing={3}
src={heroImage} alignItems="stretch"
alt="Hero" >
style={{ <Box sx={{ ...glassCardSx, flex: 1 }}>
width: "100%", <Typography sx={sectionTitleSx}>
maxHeight: "250px", 🎨 Visuel & médias du hero
objectFit: "cover", </Typography>
borderRadius: "8px", <Typography sx={{ color: "rgba(255,255,255,0.65)", mb: 2 }}>
Téléchargez depuis votre ordinateur ou collez un lien
Cloudinary pour mettre à jour l'arrière-plan principal.
</Typography>
<Box
sx={{
borderRadius: 3,
border: "1px solid rgba(255,255,255,0.2)",
overflow: "hidden",
mb: 3,
minHeight: 220,
background:
heroImage
? `url(${heroImage}) center/cover`
: "linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02))",
}} }}
/> >
{!heroImage && (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "rgba(255,255,255,0.7)",
fontWeight: 600,
letterSpacing: 1,
}}
>
Aperçu de votre visuel
</Box> </Box>
)} )}
</Box>
<Stack spacing={2}>
<Button <Button
variant="contained" variant="contained"
component="label" component="label"
sx={primaryButtonSx}
fullWidth fullWidth
sx={{ mb: 2 }}
> >
📸 Modifier l'Image Hero 📸 Importer une image
<input type="file" hidden onChange={handleImageUpload} /> <input type="file" hidden onChange={handleImageUpload} />
</Button> </Button>
<TextField <TextField
label="Ou URL Cloudinary" label="URL Cloudinary"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={heroImageCloudinaryUrl} value={heroImageCloudinaryUrl}
onChange={(e) => setHeroImageCloudinaryUrl(e.target.value)} onChange={(e) => setHeroImageCloudinaryUrl(e.target.value)}
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
placeholder="https://res.cloudinary.com/..." placeholder="https://res.cloudinary.com/..."
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<Button <Button
variant="outlined" variant="outlined"
color="primary"
fullWidth fullWidth
sx={{ mb: 2 }} sx={ghostButtonSx}
onClick={async () => { onClick={async () => {
if (heroImageCloudinaryUrl.trim()) { if (heroImageCloudinaryUrl.trim()) {
setHeroImage(heroImageCloudinaryUrl.trim()); setHeroImage(heroImageCloudinaryUrl.trim());
await updateHomePageACF(13, { await updateHomePageACF(HOMEPAGE_ID, {
img_hero_url: heroImageCloudinaryUrl.trim(), img_hero_url: heroImageCloudinaryUrl.trim(),
}); });
setSuccessMessage("✅ Image Cloudinary appliquée !"); setSuccessMessage("✅ Image Cloudinary appliquée !");
} }
}} }}
> >
🌐 Utiliser l'image Cloudinary 🌐 Utiliser l'URL renseignée
</Button> </Button>
</Stack>
</Box>
<Box sx={{ ...glassCardSx, flex: 1 }}>
<Typography sx={sectionTitleSx}>
📝 Contenu du hero
</Typography>
<Stack spacing={2}>
<TextField <TextField
label="Titre Héros" label="Titre Héros"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={heroTitle} value={heroTitle}
onChange={(e) => setHeroTitle(e.target.value)} onChange={(e) => setHeroTitle(e.target.value)}
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }} InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<TextField <TextField
@@ -293,78 +518,183 @@ const GestionPageAccueil = () => {
variant="outlined" variant="outlined"
value={heroText} value={heroText}
onChange={(e) => setHeroText(e.target.value)} onChange={(e) => setHeroText(e.target.value)}
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
multiline multiline
rows={3} minRows={3}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
</> </Stack>
</Box>
</Stack>
<Box sx={glassCardSx}>
<Typography sx={sectionTitleSx}>
🔍 Métadonnées SEO (Rank Math)
</Typography>
{seoError && (
<Typography color="error" sx={{ mb: 2 }}>
{seoError}
</Typography>
)} )}
{/* ✅ Bloc Missions */} {seoLoading ? (
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}> <Box display="flex" justifyContent="center" py={2}>
<Typography variant="h4" sx={{ marginBottom: "10px" }}> <CircularProgress size={32} />
Gestion de l'Accordéon page d'accueil </Box>
) : (
<Stack spacing={2}>
<TextField
label="Meta Title"
fullWidth
variant="outlined"
value={seoTitle}
onChange={(e) => setSeoTitle(e.target.value)}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Meta Description"
fullWidth
multiline
minRows={3}
variant="outlined"
value={seoDescription}
onChange={(e) => setSeoDescription(e.target.value)}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="URL canonique"
fullWidth
variant="outlined"
value={seoCanonical}
onChange={(e) => setSeoCanonical(e.target.value)}
placeholder={HOMEPAGE_URL}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Titre OpenGraph"
fullWidth
variant="outlined"
value={seoOgTitle}
onChange={(e) => setSeoOgTitle(e.target.value)}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Description OpenGraph"
fullWidth
multiline
minRows={3}
variant="outlined"
value={seoOgDescription}
onChange={(e) => setSeoOgDescription(e.target.value)}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Image OpenGraph (URL)"
fullWidth
variant="outlined"
value={seoOgImage}
onChange={(e) => setSeoOgImage(e.target.value)}
placeholder="https://..."
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Robots"
fullWidth
variant="outlined"
value={seoRobots}
onChange={(e) => setSeoRobots(e.target.value)}
placeholder="index,follow"
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
</Stack>
)}
</Box>
<Box sx={glassCardSx}>
<Typography sx={sectionTitleSx}>
🧩 Accordéon des missions
</Typography>
<Typography sx={{ color: "rgba(255,255,255,0.7)", mb: 2 }}>
Faites évoluer les différents blocs de votre accordéon en temps
réel.
</Typography> </Typography>
<Stack spacing={2}>
{missions.map((mission, index) => ( {missions.map((mission, index) => (
<Box <Box
key={index} key={index}
sx={{ sx={{
display: "flex", border: "1px solid rgba(255,255,255,0.15)",
alignItems: "center", borderRadius: 3,
gap: 2, p: 2,
marginBottom: 2, background: "rgba(255,255,255,0.03)",
}} }}
> >
<Stack spacing={1.5}>
<TextField <TextField
label="Titre" label={`Titre ${index + 1}`}
variant="outlined" variant="outlined"
fullWidth fullWidth
value={mission.title} value={mission.title}
onChange={(e) => updateMission(index, "title", e.target.value)} onChange={(e) =>
updateMission(index, "title", e.target.value)
}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<TextField <TextField
label="Détails" label="Détails"
variant="outlined" variant="outlined"
fullWidth fullWidth
multiline
minRows={2}
value={mission.details} value={mission.details}
onChange={(e) => onChange={(e) =>
updateMission(index, "details", e.target.value) updateMission(index, "details", e.target.value)
} }
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<Button <Button
onClick={() => deleteMission(index)} onClick={() => deleteMission(index)}
variant="contained"
color="error" color="error"
variant="contained"
sx={{ borderRadius: 2, textTransform: "none" }}
> >
Supprimer Supprimer
</Button> </Button>
</Stack>
</Box> </Box>
))} ))}
<Button <Button
variant="contained" variant="contained"
color="primary"
onClick={addMission} onClick={addMission}
sx={{ marginTop: "10px" }} sx={primaryButtonSx}
> >
Ajouter une mission Ajouter une mission
</Button> </Button>
</Paper> </Stack>
</Box>
{/* ✅ Bloc Construct */}
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
<Typography variant="h4" sx={{ marginBottom: "10px" }}>
🔧 Zone Formations
</Typography>
<Box sx={glassCardSx}>
<Typography sx={sectionTitleSx}>🔧 Zone Formations</Typography>
<Stack spacing={2}>
<TextField <TextField
label="Titre h2" label="Titre h2"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={constructTitle} value={constructTitle}
onChange={(e) => setConstructTitle(e.target.value)} onChange={(e) => setConstructTitle(e.target.value)}
sx={{ mb: 2 }} InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<TextField <TextField
@@ -373,7 +703,8 @@ const GestionPageAccueil = () => {
variant="outlined" variant="outlined"
value={constructSubtitle} value={constructSubtitle}
onChange={(e) => setConstructSubtitle(e.target.value)} onChange={(e) => setConstructSubtitle(e.target.value)}
sx={{ mb: 2 }} InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<TextField <TextField
@@ -382,9 +713,11 @@ const GestionPageAccueil = () => {
variant="outlined" variant="outlined"
value={constructSector} value={constructSector}
onChange={(e) => setConstructSector(e.target.value)} onChange={(e) => setConstructSector(e.target.value)}
sx={{ mb: 2 }} InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/> />
<Box>
<Typography sx={{ mb: 1, fontWeight: "bold" }}> <Typography sx={{ mb: 1, fontWeight: "bold" }}>
Texte Construct Texte Construct
</Typography> </Typography>
@@ -392,150 +725,113 @@ const GestionPageAccueil = () => {
theme="snow" theme="snow"
value={constructText} value={constructText}
onChange={setConstructText} onChange={setConstructText}
style={{ style={quillStyle}
marginBottom: "20px",
backgroundColor: "#fff",
borderRadius: "8px",
}}
/> />
</Box>
<Box>
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Notes</Typography> <Typography sx={{ mb: 1, fontWeight: "bold" }}>Notes</Typography>
<ReactQuill <ReactQuill
theme="snow" theme="snow"
value={constructNote} value={constructNote}
onChange={setConstructNote} onChange={setConstructNote}
style={{ style={quillStyle}
marginBottom: "20px",
backgroundColor: "#fff",
borderRadius: "8px",
}}
/> />
</Paper> </Box>
</Stack>
</Box>
{/* ✅ Bloc Expertises */} <Box sx={glassCardSx}>
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}> <Typography sx={sectionTitleSx}>🧠 Zone Expertises</Typography>
<Typography variant="h4" sx={{ marginBottom: "10px" }}> <Stack spacing={3}>
🔧 Zone Expertises
</Typography>
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
<TextField <TextField
label="Titre h2" label="Titre global"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={expertiseTitle} value={expertiseTitle}
onChange={(e) => setexpertiseTitle(e.target.value)} onChange={(e) => setexpertiseTitle(e.target.value)}
sx={{ mb: 2 }} InputLabelProps={{ sx: labelSx }}
/> sx={textFieldSx}
</Paper>
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
<TextField
label="Titre h2"
fullWidth
variant="outlined"
value={expertise1Title}
onChange={(e) => setexpertise1Title(e.target.value)}
sx={{ mb: 2 }}
/> />
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Texte h2</Typography> {[1, 2, 3].map((index) => {
<ReactQuill const titleState =
theme="snow" index === 1
value={expertise1Text} ? expertise1Title
onChange={setexpertise1Text} : index === 2
style={{ ? expertise2Title
marginBottom: "20px", : expertise3Title;
backgroundColor: "#fff", const textState =
borderRadius: "8px", index === 1
}} ? expertise1Text
/> : index === 2
</Paper> ? expertise2Text
: expertise3Text;
const setTitle =
index === 1
? setexpertise1Title
: index === 2
? setexpertise2Title
: setexpertise3Title;
const setText =
index === 1
? setexpertise1Text
: index === 2
? setexpertise2Text
: setexpertise3Text;
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}> return (
<TextField <Box
label="Titre h2" key={index}
fullWidth
variant="outlined"
value={expertise2Title}
onChange={(e) => setexpertise2Title(e.target.value)}
sx={{ mb: 2 }}
/>
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Texte h2</Typography>
<ReactQuill
theme="snow"
value={expertise2Text}
onChange={setexpertise2Text}
style={{
marginBottom: "20px",
backgroundColor: "#fff",
borderRadius: "8px",
}}
/>
</Paper>
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
<TextField
label="Titre h2"
fullWidth
variant="outlined"
value={expertise3Title}
onChange={(e) => setexpertise3Title(e.target.value)}
sx={{ mb: 2 }}
/>
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Texte h2</Typography>
<ReactQuill
theme="snow"
value={expertise3Text}
onChange={setexpertise3Text}
style={{
marginBottom: "20px",
backgroundColor: "#fff",
borderRadius: "8px",
}}
/>
</Paper>
</Paper>
{/* ✅ Actions */}
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
<Button
variant="contained"
color="primary"
fullWidth
sx={{ sx={{
mt: 3, border: "1px solid rgba(255,255,255,0.15)",
fontSize: "1.1rem", borderRadius: 3,
fontWeight: "bold", p: { xs: 2, md: 3 },
color: "#ffffff", background: "rgba(255,255,255,0.03)",
backgroundColor: "#0e467f",
"&:hover": { backgroundColor: "#093a6b" },
}} }}
onClick={handleSave}
> >
<TextField
label={`Titre expertise ${index}`}
fullWidth
variant="outlined"
value={titleState}
onChange={(e) => setTitle(e.target.value)}
InputLabelProps={{ sx: labelSx }}
sx={[textFieldSx, { mb: 2 }]}
/>
<Typography sx={{ mb: 1, fontWeight: "bold" }}>
Texte expertise {index}
</Typography>
<ReactQuill
theme="snow"
value={textState}
onChange={setText}
style={quillStyle}
/>
</Box>
);
})}
</Stack>
</Box>
<Box sx={{ ...glassCardSx, textAlign: "center" }}>
<Stack spacing={2}>
<Button variant="contained" sx={primaryButtonSx} onClick={handleSave}>
💾 Enregistrer les modifications 💾 Enregistrer les modifications
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
color="secondary" sx={ghostButtonSx}
fullWidth
sx={{
mt: 2,
fontSize: "1rem",
fontWeight: "bold",
borderColor: "#0e467f",
color: "#0e467f",
}}
onClick={() => navigate("/admin/dashboard")} onClick={() => navigate("/admin/dashboard")}
> >
Retour au Dashboard Retour au Dashboard
</Button> </Button>
</Paper> </Stack>
</Paper> </Box>
</Stack>
)}
</Container> </Container>
</Box>
); );
}; };
+7 -4
View File
@@ -31,7 +31,7 @@ const Home = () => {
// 🔥 Ajout d'un timestamp pour éviter la mise en cache // 🔥 Ajout d'un timestamp pour éviter la mise en cache
const response = await api.get( const response = await api.get(
`wp/v2/pages/13?_fields=acf,rank_math_title,rank_math_description&_=${new Date().getTime()}` `wp/v2/pages/13?_fields=acf,link,rank_math_title,rank_math_description&_=${new Date().getTime()}`
); );
const pageContent = response.data; const pageContent = response.data;
setPageData(pageContent); setPageData(pageContent);
@@ -79,19 +79,22 @@ const Home = () => {
); );
} }
const { acf, rank_math_title, rank_math_description } = pageData || {}; const { acf, rank_math_title, rank_math_description, link } = pageData || {};
const fallbackUrl =
typeof window !== "undefined" ? window.location.href : "";
const pageUrl = link || fallbackUrl;
return ( return (
<div> <div>
{/* SEO */} {/* SEO */}
<SEO <SEO
postId={13} // ID WordPress valide pageUrl={pageUrl}
defaultTitle={rank_math_title || "centre de formation "} defaultTitle={rank_math_title || "centre de formation "}
defaultDescription={ defaultDescription={
rank_math_description || rank_math_description ||
"formation et reconversion professionnelle dans les métiers du numérique" "formation et reconversion professionnelle dans les métiers du numérique"
} }
defaultCanonicalUrl={acf?.canonical_url || window.location.href} defaultCanonicalUrl={acf?.canonical_url || pageUrl}
defaultOgImage={ defaultOgImage={
heroImage || heroImage ||
"https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif" "https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif"
+124 -42
View File
@@ -1,71 +1,153 @@
import React, { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import axios from "axios"; import api from "../api";
const DEFAULT_OG_IMAGE =
"https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif";
const getWindowHref = () =>
typeof window !== "undefined" ? window.location.href : "";
const SEO = ({ const SEO = ({
postId, pageUrl,
defaultTitle = "Titre par défaut", defaultTitle = "Titre par défaut",
defaultDescription = "Description par défaut", defaultDescription = "Description par défaut",
defaultCanonicalUrl = "", defaultCanonicalUrl = "",
defaultOgImage = "https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif" defaultOgImage = DEFAULT_OG_IMAGE,
}) => { }) => {
const [rankMathHead, setRankMathHead] = useState(null);
const [shouldFallback, setShouldFallback] = useState(true);
const [metaData, setMetaData] = useState({ const resolvedCanonical = defaultCanonicalUrl || getWindowHref();
title: defaultTitle, const targetUrl = pageUrl || resolvedCanonical;
description: defaultDescription,
canonicalUrl: defaultCanonicalUrl,
ogTitle: defaultTitle,
ogDescription: defaultDescription,
ogImage: defaultOgImage,
});
useEffect(() => { useEffect(() => {
if (!postId) return; if (!targetUrl) return;
const fetchMetaData = async () => { const controller = new AbortController();
const fetchRankMathHead = async () => {
try { try {
const { data } = await axios.get( const { data } = await api.get("rankmath/v1/getHead", {
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${postId}` params: { url: targetUrl },
); signal: controller.signal,
});
if (data && data.rank_math_og) { if (
setMetaData((prev) => ({ data?.success &&
...prev, data.head &&
title: data.rank_math_og.og_title || prev.title, typeof window !== "undefined" &&
description: data.rank_math_og.og_description || prev.description, window.DOMParser
ogTitle: data.rank_math_og.og_title || prev.title, ) {
ogDescription: data.rank_math_og.og_description || prev.description, const parser = new window.DOMParser();
ogImage: data.rank_math_og.og_image || prev.ogImage, const parsedDocument = parser.parseFromString(
canonicalUrl: data.acf?.canonical_url || window.location.href `<!doctype html><html><head>${data.head}</head><body></body></html>`,
"text/html"
);
const headElement = parsedDocument.head;
const attributesToObject = (element) =>
Array.from(element.attributes).reduce((acc, attr) => {
acc[attr.name] = attr.value;
return acc;
}, {});
const metaTags = Array.from(headElement.querySelectorAll("meta")).map(
(meta) => attributesToObject(meta)
);
const linkTags = Array.from(headElement.querySelectorAll("link")).map(
(link) => attributesToObject(link)
);
const scriptTags = Array.from(
headElement.querySelectorAll("script")
).map((script) => ({
attributes: attributesToObject(script),
innerHTML: script.innerHTML,
})); }));
setRankMathHead({
title: headElement.querySelector("title")?.textContent || "",
meta: metaTags,
links: linkTags,
scripts: scriptTags,
});
setShouldFallback(false);
} else {
setRankMathHead(null);
setShouldFallback(true);
} }
} catch (error) { } catch (error) {
console.error("⚠️ Erreur lors de la récupération des métadonnées SEO :", error); if (!controller.signal.aborted) {
console.error(
"⚠️ Erreur lors de la récupération des métadonnées Rank Math :",
error
);
}
setRankMathHead(null);
setShouldFallback(true);
} }
}; };
fetchMetaData(); fetchRankMathHead();
}, [postId]);
return () => controller.abort();
}, [targetUrl]);
const rankMathNodes = useMemo(() => {
if (!rankMathHead) return null;
const renderMetaTags = rankMathHead.meta.map((attributes, index) => (
<meta key={`rank-math-meta-${index}`} {...attributes} />
));
const renderLinkTags = rankMathHead.links.map((attributes, index) => (
<link key={`rank-math-link-${index}`} {...attributes} />
));
const renderScriptTags = rankMathHead.scripts.map((script, index) => (
<script
key={`rank-math-script-${index}`}
{...script.attributes}
dangerouslySetInnerHTML={{ __html: script.innerHTML }}
/>
));
return (
<>
{rankMathHead.title && <title>{rankMathHead.title}</title>}
{renderMetaTags}
{renderLinkTags}
{renderScriptTags}
</>
);
}, [rankMathHead]);
return ( return (
<Helmet> <Helmet>
{/* SEO standard */} {!shouldFallback && rankMathNodes}
<title>{metaData.title}</title>
<meta name="description" content={metaData.description} /> {shouldFallback && (
{metaData.canonicalUrl && <link rel="canonical" href={metaData.canonicalUrl} />} <>
<title>{defaultTitle}</title>
<meta name="description" content={defaultDescription} />
{resolvedCanonical && (
<link rel="canonical" href={resolvedCanonical} />
)}
{/* Open Graph (Facebook, LinkedIn) */}
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content={metaData.ogTitle} /> <meta property="og:title" content={defaultTitle} />
<meta property="og:description" content={metaData.ogDescription} /> <meta property="og:description" content={defaultDescription} />
<meta property="og:image" content={metaData.ogImage} /> <meta property="og:image" content={defaultOgImage} />
<meta property="og:url" content={metaData.canonicalUrl} /> {resolvedCanonical && (
<meta property="og:url" content={resolvedCanonical} />
)}
{/* Twitter Cards */}
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={metaData.ogTitle} /> <meta name="twitter:title" content={defaultTitle} />
<meta name="twitter:description" content={metaData.ogDescription} /> <meta name="twitter:description" content={defaultDescription} />
<meta name="twitter:image" content={metaData.ogImage} /> <meta name="twitter:image" content={defaultOgImage} />
</>
)}
</Helmet> </Helmet>
); );
}; };
+50
View File
@@ -2,6 +2,7 @@ import axios from "axios";
import { getToken } from "./auth"; // 🔥 Import du token pour l'authentification import { getToken } from "./auth"; // 🔥 Import du token pour l'authentification
const API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2"; const API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2";
const RANKMATH_API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json/rankmath/v1";
/** /**
* 🔹 Upload une image et retourne son ID * 🔹 Upload une image et retourne son ID
@@ -334,3 +335,52 @@ export async function fetchPages() {
return []; // ✅ Retourne un tableau vide en cas d'erreur pour éviter le plantage return []; // ✅ Retourne un tableau vide en cas d'erreur pour éviter le plantage
} }
} }
/**
* 🔹 Met à jour les métadonnées Rank Math (SEO) d'une page/post.
* @param {number} objectId - ID WordPress de la page.
* @param {object} meta - Objet des champs Rank Math à modifier.
*/
export async function updateRankMathMeta(objectId, meta = {}) {
const token = getToken();
if (!token) {
console.error("❌ Aucun token Rank Math trouvé !");
throw new Error("Utilisateur non authentifié.");
}
const sanitizedMeta = Object.entries(meta).reduce((acc, [key, value]) => {
const cleanedValue = typeof value === "string" ? value.trim() : value;
if (cleanedValue) {
acc[key] = cleanedValue;
}
return acc;
}, {});
if (!Object.keys(sanitizedMeta).length) {
console.warn("️ Aucun champ Rank Math fourni, mise à jour ignorée.");
return null;
}
try {
const response = await axios.post(
`${RANKMATH_API_URL}/updateMeta`,
{
objectType: "post", // ✅ Les pages WordPress sont du type "post".
objectID: objectId,
meta: sanitizedMeta,
},
{
headers: {
Authorization: `Basic ${token}`,
"Content-Type": "application/json",
},
}
);
console.log("✅ Métadonnées Rank Math mises à jour :", response.data);
return response.data;
} catch (error) {
console.error("❌ Erreur mise à jour Rank Math :", error.response?.data || error.message);
throw error;
}
}