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 { updateHomePageACF, uploadImage } from "../../wordpress";
|
||||
import { updateHomePageACF, updateRankMathMeta, uploadImage } from "../../wordpress";
|
||||
import { getToken } from "../../auth";
|
||||
import api from "../../api";
|
||||
import {
|
||||
Typography,
|
||||
TextField,
|
||||
@@ -10,10 +11,14 @@ import {
|
||||
Paper,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
const HOMEPAGE_ID = 13;
|
||||
const HOMEPAGE_URL = "https://preprod.octopusdesign.fr/api-octopus/server/";
|
||||
|
||||
const GestionPageAccueil = () => {
|
||||
const navigate = useNavigate();
|
||||
const [heroTitle, setHeroTitle] = useState("");
|
||||
@@ -39,6 +44,132 @@ const GestionPageAccueil = () => {
|
||||
const [expertise3Title, setexpertise3Title] = 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é
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
@@ -51,7 +182,7 @@ const GestionPageAccueil = () => {
|
||||
const fetchPageData = async () => {
|
||||
try {
|
||||
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",
|
||||
headers: {
|
||||
@@ -106,11 +237,15 @@ const GestionPageAccueil = () => {
|
||||
};
|
||||
|
||||
fetchPageData();
|
||||
}, []);
|
||||
loadSeoMeta();
|
||||
}, [loadSeoMeta]);
|
||||
|
||||
// ✅ Enregistrement ACF
|
||||
// ✅ Enregistrement ACF + Rank Math
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
setSuccessMessage("");
|
||||
|
||||
const newData = {
|
||||
hero_title: heroTitle,
|
||||
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));
|
||||
setSuccessMessage("✅ Modifications enregistrées avec succès !");
|
||||
await loadSeoMeta();
|
||||
|
||||
setSuccessMessage("✅ Contenu & métadonnées enregistrés !");
|
||||
setTimeout(() => setSuccessMessage(""), 3000);
|
||||
} catch (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 {
|
||||
const imageId = await uploadImage(file);
|
||||
setHeroImage(URL.createObjectURL(file));
|
||||
await updateHomePageACF(13, { img_hero: imageId });
|
||||
await updateHomePageACF(HOMEPAGE_ID, { img_hero: imageId });
|
||||
setSuccessMessage("✅ Image mise à jour !");
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur upload image :", error);
|
||||
@@ -181,25 +332,60 @@ const GestionPageAccueil = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ mt: 16, mb: 5 }}>
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{ padding: 4, borderRadius: 3, backgroundColor: "#f8f9fa" }}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
<Box
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
mb: 3,
|
||||
color: "#0e467f",
|
||||
minHeight: "100vh",
|
||||
py: { xs: 4, md: 8 },
|
||||
px: { xs: 2, md: 4 },
|
||||
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 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 ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
py={6}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : error ? (
|
||||
@@ -207,84 +393,123 @@ const GestionPageAccueil = () => {
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Stack spacing={4}>
|
||||
{successMessage && (
|
||||
<Box
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
backgroundColor: "#d4edda",
|
||||
color: "#155724",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
marginBottom: "16px",
|
||||
...glassCardSx,
|
||||
borderColor: "rgba(72, 255, 184, 0.6)",
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(48,199,135,0.25), rgba(43,170,241,0.25))",
|
||||
textAlign: "center",
|
||||
boxShadow: "0px 2px 10px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" fontWeight="bold">
|
||||
<Typography fontWeight="bold" color="#e5fff5">
|
||||
{successMessage}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{heroImage && (
|
||||
<Box sx={{ textAlign: "center", mb: 2 }}>
|
||||
<img
|
||||
src={heroImage}
|
||||
alt="Hero"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxHeight: "250px",
|
||||
objectFit: "cover",
|
||||
borderRadius: "8px",
|
||||
<Stack
|
||||
direction={{ xs: "column", lg: "row" }}
|
||||
spacing={3}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<Box sx={{ ...glassCardSx, flex: 1 }}>
|
||||
<Typography sx={sectionTitleSx}>
|
||||
🎨 Visuel & médias du hero
|
||||
</Typography>
|
||||
<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>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
component="label"
|
||||
sx={primaryButtonSx}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
📸 Modifier l'Image Hero
|
||||
📸 Importer une image
|
||||
<input type="file" hidden onChange={handleImageUpload} />
|
||||
</Button>
|
||||
|
||||
<TextField
|
||||
label="Ou URL Cloudinary"
|
||||
label="URL Cloudinary"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={heroImageCloudinaryUrl}
|
||||
onChange={(e) => setHeroImageCloudinaryUrl(e.target.value)}
|
||||
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
|
||||
placeholder="https://res.cloudinary.com/..."
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
sx={ghostButtonSx}
|
||||
onClick={async () => {
|
||||
if (heroImageCloudinaryUrl.trim()) {
|
||||
setHeroImage(heroImageCloudinaryUrl.trim());
|
||||
await updateHomePageACF(13, {
|
||||
await updateHomePageACF(HOMEPAGE_ID, {
|
||||
img_hero_url: heroImageCloudinaryUrl.trim(),
|
||||
});
|
||||
setSuccessMessage("✅ Image Cloudinary appliquée !");
|
||||
}
|
||||
}}
|
||||
>
|
||||
🌐 Utiliser l'image Cloudinary
|
||||
🌐 Utiliser l'URL renseignée
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ ...glassCardSx, flex: 1 }}>
|
||||
<Typography sx={sectionTitleSx}>
|
||||
📝 Contenu du hero
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Titre Héros"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={heroTitle}
|
||||
onChange={(e) => setHeroTitle(e.target.value)}
|
||||
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -293,78 +518,183 @@ const GestionPageAccueil = () => {
|
||||
variant="outlined"
|
||||
value={heroText}
|
||||
onChange={(e) => setHeroText(e.target.value)}
|
||||
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
|
||||
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 */}
|
||||
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
|
||||
<Typography variant="h4" sx={{ marginBottom: "10px" }}>
|
||||
Gestion de l'Accordéon page d'accueil
|
||||
{seoLoading ? (
|
||||
<Box display="flex" justifyContent="center" py={2}>
|
||||
<CircularProgress size={32} />
|
||||
</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>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{missions.map((mission, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
marginBottom: 2,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: 3,
|
||||
p: 2,
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.5}>
|
||||
<TextField
|
||||
label="Titre"
|
||||
label={`Titre ${index + 1}`}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
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
|
||||
label="Détails"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
value={mission.details}
|
||||
onChange={(e) =>
|
||||
updateMission(index, "details", e.target.value)
|
||||
}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => deleteMission(index)}
|
||||
variant="contained"
|
||||
color="error"
|
||||
variant="contained"
|
||||
sx={{ borderRadius: 2, textTransform: "none" }}
|
||||
>
|
||||
❌ Supprimer
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={addMission}
|
||||
sx={{ marginTop: "10px" }}
|
||||
sx={primaryButtonSx}
|
||||
>
|
||||
➕ Ajouter une mission
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* ✅ Bloc Construct */}
|
||||
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
|
||||
<Typography variant="h4" sx={{ marginBottom: "10px" }}>
|
||||
🔧 Zone Formations
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={glassCardSx}>
|
||||
<Typography sx={sectionTitleSx}>🔧 Zone Formations</Typography>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Titre h2"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={constructTitle}
|
||||
onChange={(e) => setConstructTitle(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -373,7 +703,8 @@ const GestionPageAccueil = () => {
|
||||
variant="outlined"
|
||||
value={constructSubtitle}
|
||||
onChange={(e) => setConstructSubtitle(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -382,9 +713,11 @@ const GestionPageAccueil = () => {
|
||||
variant="outlined"
|
||||
value={constructSector}
|
||||
onChange={(e) => setConstructSector(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Typography sx={{ mb: 1, fontWeight: "bold" }}>
|
||||
Texte Construct
|
||||
</Typography>
|
||||
@@ -392,150 +725,113 @@ const GestionPageAccueil = () => {
|
||||
theme="snow"
|
||||
value={constructText}
|
||||
onChange={setConstructText}
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
style={quillStyle}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Notes</Typography>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={constructNote}
|
||||
onChange={setConstructNote}
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
style={quillStyle}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* ✅ Bloc Expertises */}
|
||||
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
|
||||
<Typography variant="h4" sx={{ marginBottom: "10px" }}>
|
||||
🔧 Zone Expertises
|
||||
</Typography>
|
||||
|
||||
<Paper elevation={3} sx={{ marginTop: "30px", padding: "20px" }}>
|
||||
<Box sx={glassCardSx}>
|
||||
<Typography sx={sectionTitleSx}>🧠 Zone Expertises</Typography>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="Titre h2"
|
||||
label="Titre global"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={expertiseTitle}
|
||||
onChange={(e) => setexpertiseTitle(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
</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 }}
|
||||
InputLabelProps={{ sx: labelSx }}
|
||||
sx={textFieldSx}
|
||||
/>
|
||||
|
||||
<Typography sx={{ mb: 1, fontWeight: "bold" }}>Texte h2</Typography>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={expertise1Text}
|
||||
onChange={setexpertise1Text}
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
{[1, 2, 3].map((index) => {
|
||||
const titleState =
|
||||
index === 1
|
||||
? expertise1Title
|
||||
: index === 2
|
||||
? expertise2Title
|
||||
: expertise3Title;
|
||||
const textState =
|
||||
index === 1
|
||||
? expertise1Text
|
||||
: index === 2
|
||||
? 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" }}>
|
||||
<TextField
|
||||
label="Titre h2"
|
||||
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
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
mt: 3,
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: "bold",
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#0e467f",
|
||||
"&:hover": { backgroundColor: "#093a6b" },
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: 3,
|
||||
p: { xs: 2, md: 3 },
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
}}
|
||||
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
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
borderColor: "#0e467f",
|
||||
color: "#0e467f",
|
||||
}}
|
||||
sx={ghostButtonSx}
|
||||
onClick={() => navigate("/admin/dashboard")}
|
||||
>
|
||||
⬅ Retour au Dashboard
|
||||
</Button>
|
||||
</Paper>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const Home = () => {
|
||||
|
||||
// 🔥 Ajout d'un timestamp pour éviter la mise en cache
|
||||
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;
|
||||
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 (
|
||||
<div>
|
||||
{/* SEO */}
|
||||
<SEO
|
||||
postId={13} // ID WordPress valide
|
||||
pageUrl={pageUrl}
|
||||
defaultTitle={rank_math_title || "centre de formation "}
|
||||
defaultDescription={
|
||||
rank_math_description ||
|
||||
"formation et reconversion professionnelle dans les métiers du numérique"
|
||||
}
|
||||
defaultCanonicalUrl={acf?.canonical_url || window.location.href}
|
||||
defaultCanonicalUrl={acf?.canonical_url || pageUrl}
|
||||
defaultOgImage={
|
||||
heroImage ||
|
||||
"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 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 = ({
|
||||
postId,
|
||||
pageUrl,
|
||||
defaultTitle = "Titre par défaut",
|
||||
defaultDescription = "Description par défaut",
|
||||
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({
|
||||
title: defaultTitle,
|
||||
description: defaultDescription,
|
||||
canonicalUrl: defaultCanonicalUrl,
|
||||
ogTitle: defaultTitle,
|
||||
ogDescription: defaultDescription,
|
||||
ogImage: defaultOgImage,
|
||||
});
|
||||
const resolvedCanonical = defaultCanonicalUrl || getWindowHref();
|
||||
const targetUrl = pageUrl || resolvedCanonical;
|
||||
|
||||
useEffect(() => {
|
||||
if (!postId) return;
|
||||
if (!targetUrl) return;
|
||||
|
||||
const fetchMetaData = async () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchRankMathHead = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${postId}`
|
||||
);
|
||||
const { data } = await api.get("rankmath/v1/getHead", {
|
||||
params: { url: targetUrl },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (data && data.rank_math_og) {
|
||||
setMetaData((prev) => ({
|
||||
...prev,
|
||||
title: data.rank_math_og.og_title || prev.title,
|
||||
description: data.rank_math_og.og_description || prev.description,
|
||||
ogTitle: data.rank_math_og.og_title || prev.title,
|
||||
ogDescription: data.rank_math_og.og_description || prev.description,
|
||||
ogImage: data.rank_math_og.og_image || prev.ogImage,
|
||||
canonicalUrl: data.acf?.canonical_url || window.location.href
|
||||
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 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) {
|
||||
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();
|
||||
}, [postId]);
|
||||
fetchRankMathHead();
|
||||
|
||||
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 (
|
||||
<Helmet>
|
||||
{/* SEO standard */}
|
||||
<title>{metaData.title}</title>
|
||||
<meta name="description" content={metaData.description} />
|
||||
{metaData.canonicalUrl && <link rel="canonical" href={metaData.canonicalUrl} />}
|
||||
{!shouldFallback && rankMathNodes}
|
||||
|
||||
{shouldFallback && (
|
||||
<>
|
||||
<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:title" content={metaData.ogTitle} />
|
||||
<meta property="og:description" content={metaData.ogDescription} />
|
||||
<meta property="og:image" content={metaData.ogImage} />
|
||||
<meta property="og:url" content={metaData.canonicalUrl} />
|
||||
<meta property="og:title" content={defaultTitle} />
|
||||
<meta property="og:description" content={defaultDescription} />
|
||||
<meta property="og:image" content={defaultOgImage} />
|
||||
{resolvedCanonical && (
|
||||
<meta property="og:url" content={resolvedCanonical} />
|
||||
)}
|
||||
|
||||
{/* Twitter Cards */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={metaData.ogTitle} />
|
||||
<meta name="twitter:description" content={metaData.ogDescription} />
|
||||
<meta name="twitter:image" content={metaData.ogImage} />
|
||||
<meta name="twitter:title" content={defaultTitle} />
|
||||
<meta name="twitter:description" content={defaultDescription} />
|
||||
<meta name="twitter:image" content={defaultOgImage} />
|
||||
</>
|
||||
)}
|
||||
</Helmet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from "axios";
|
||||
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 RANKMATH_API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json/rankmath/v1";
|
||||
|
||||
/**
|
||||
* 🔹 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔹 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