integration SEORank
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user