mise a jour structure SEO

This commit is contained in:
sebvtl728
2025-11-05 01:08:39 +01:00
parent 5909871dec
commit 1890d830e1
12 changed files with 3157 additions and 205 deletions
+168 -6
View File
@@ -6,6 +6,7 @@ import { Box, Typography, CircularProgress } from "@mui/material";
const API_URL =
"https://www.formations.octopusdesign.fr/webservice/rest/server.php";
const TOKEN = "685e1b5d794b558b60e971581154c3b2";
const PUBLIC_LINKS_STORAGE_KEY = "listeCoursPublicLinks";
const CoursLecture = () => {
// Empêche le clic droit
@@ -23,6 +24,7 @@ const CoursLecture = () => {
const { token } = useParams();
const [content, setContent] = useState(null);
const [title, setTitle] = useState("");
const [loading, setLoading] = useState(true);
const [tokenValid, setTokenValid] = useState(true);
@@ -46,15 +48,131 @@ const CoursLecture = () => {
// ✅ Récupération contenu
useEffect(() => {
const [courseId, pageId, expiry] = atob(token).split("_");
const decodeToken = () => {
if (!token) {
return null;
}
try {
const raw = atob(token);
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") {
throw new Error("Invalid JSON payload");
}
const {
courseId,
itemType = "page",
itemId,
expiry,
linkId = null,
} = parsed;
if (!courseId || !itemId || !expiry) {
return null;
}
return {
courseId: String(courseId),
itemType: itemType || "page",
itemId: String(itemId),
expiry,
linkId,
format: "json",
};
} catch (jsonError) {
const parts = raw.split("_");
if (parts.length === 3) {
const [courseId, itemId, expiry] = parts;
return {
courseId,
itemType: "page",
itemId,
expiry,
linkId: null,
format: "legacy",
};
}
if (parts.length === 4) {
const [courseId, itemType, itemId, expiry] = parts;
return {
courseId,
itemType,
itemId,
expiry,
linkId: null,
format: "legacy",
};
}
return null;
}
} catch (error) {
return null;
}
};
if (!courseId || !pageId || !expiry || Date.now() > parseInt(expiry)) {
const decoded = decodeToken();
if (!decoded) {
setTokenValid(false);
setContent("⛔ Lien invalide.");
setLoading(false);
return;
}
const { courseId, itemType, itemId, expiry, format } = decoded;
const expiryValue = Number(expiry);
if (
!courseId ||
!itemId ||
!Number.isFinite(expiryValue) ||
Date.now() > expiryValue
) {
setTokenValid(false);
setContent("⛔ Le cours est terminé !");
setLoading(false);
return;
}
const normalizedItemType = itemType || "page";
const linkStillActive = () => {
if (format === "legacy") {
return true;
}
if (typeof window === "undefined") {
return true;
}
try {
const stored = localStorage.getItem(PUBLIC_LINKS_STORAGE_KEY);
if (!stored) {
return false;
}
const registry = JSON.parse(stored);
const key = `${courseId}-${normalizedItemType}-${itemId}`;
const entry = registry?.[key];
if (!entry || !entry.active) {
return false;
}
if (entry.token && entry.token !== token) {
return false;
}
if (
entry.expiry !== undefined &&
Number.isFinite(Number(entry.expiry)) &&
Date.now() > Number(entry.expiry)
) {
return false;
}
return true;
} catch (error) {
console.error("Impossible de vérifier l'état du lien public :", error);
return false;
}
};
if (!linkStillActive()) {
setTokenValid(false);
setContent("⛔ Ce lien n'est plus actif.");
setLoading(false);
return;
}
const fetchPage = async () => {
try {
const res = await axios.get(API_URL, {
@@ -62,7 +180,7 @@ const CoursLecture = () => {
wstoken: TOKEN,
wsfunction: "mod_page_get_pages_by_courses",
moodlewsrestformat: "json",
courseids: [parseInt(courseId)],
courseids: [parseInt(courseId, 10)],
},
paramsSerializer: (params) => {
const sp = new URLSearchParams();
@@ -77,9 +195,12 @@ const CoursLecture = () => {
},
});
const found = res.data.pages.find((p) => p.id === parseInt(pageId));
const found = (res.data.pages || []).find(
(p) => p.id === parseInt(itemId, 10)
);
if (found) {
setContent(found.content);
setTitle(found.name || "");
setContent(found.content || "");
} else {
setTokenValid(false);
setContent("⛔ Page non trouvée.");
@@ -92,7 +213,43 @@ const CoursLecture = () => {
}
};
fetchPage();
const fetchAssignment = async () => {
try {
const res = await axios.get(API_URL, {
params: {
wstoken: TOKEN,
wsfunction: "mod_assign_get_assignments",
moodlewsrestformat: "json",
},
});
const parsedCourseId = parseInt(courseId, 10);
const parsedAssignmentId = parseInt(itemId, 10);
const course = (res.data.courses || []).find(
(c) => c.id === parsedCourseId
);
const assignment = (course?.assignments || []).find(
(a) => a.id === parsedAssignmentId
);
if (assignment) {
setTitle(assignment.name || "");
setContent(assignment.intro || "<p>Aucun contenu disponible.</p>");
} else {
setTokenValid(false);
setContent("⛔ Devoir introuvable.");
}
} catch (error) {
setTokenValid(false);
setContent("❌ Erreur lors du chargement du devoir.");
} finally {
setLoading(false);
}
};
if (normalizedItemType === "assignment") {
fetchAssignment();
} else {
fetchPage();
}
}, [token]);
if (loading) {
@@ -140,6 +297,11 @@ const CoursLecture = () => {
return (
<Box sx={{ padding: 4, mt: 5 }}>
{title ? (
<Typography variant="h4" sx={{ mb: 3, color: "#315397" }}>
{title}
</Typography>
) : null}
<div dangerouslySetInnerHTML={{ __html: content }} />
</Box>
);
File diff suppressed because it is too large Load Diff
+129 -7
View File
@@ -2,12 +2,92 @@ import { useEffect, useMemo, useState } from "react";
import { Helmet } from "react-helmet-async";
import api from "../api";
const PRODUCTION_ORIGIN = "https://octopusdesign.fr";
const PREPROD_HOST = "preprod.octopusdesign.fr";
const PATH_PREFIX_TO_STRIP = "/api-octopus/server";
const FORCE_NO_INDEX = true;
const DEFAULT_OG_IMAGE =
"https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif";
const DEFAULT_ROBOTS = FORCE_NO_INDEX ? "noindex, nofollow" : "index, follow";
const getWindowHref = () =>
typeof window !== "undefined" ? window.location.href : "";
const shouldNormalize = (value) =>
typeof value === "string" &&
(value.includes(PREPROD_HOST) || value.includes(PATH_PREFIX_TO_STRIP) || value.startsWith("http://127.") || value.includes("localhost"));
const normalizeToProduction = (value) => {
if (typeof value !== "string" || value.length === 0) return value;
const trimmed = value.trim();
const keepTrailingSlash = trimmed.endsWith("/") && !trimmed.endsWith("//");
try {
const reference = new URL(PRODUCTION_ORIGIN);
const url = new URL(trimmed, PRODUCTION_ORIGIN);
const mustForceProductionHost =
url.hostname === PREPROD_HOST ||
url.hostname === "127.0.0.1" ||
url.hostname === "localhost";
if (mustForceProductionHost) {
url.protocol = reference.protocol;
url.hostname = reference.hostname;
url.port = "";
}
if (url.pathname.startsWith(PATH_PREFIX_TO_STRIP)) {
const stripped = url.pathname.replace(PATH_PREFIX_TO_STRIP, "") || "/";
url.pathname = stripped;
}
const shouldAppendSlash =
!url.pathname.includes(".") &&
url.pathname !== "/" &&
!url.pathname.endsWith("/") &&
(keepTrailingSlash || mustForceProductionHost || url.hostname === reference.hostname);
if (shouldAppendSlash) {
url.pathname = `${url.pathname}/`;
}
if (keepTrailingSlash && url.pathname === "/") {
url.pathname = "/";
}
const normalized = url.toString().replace(/([^:]\/)\/+/g, "$1");
return normalized;
} catch (error) {
if (shouldNormalize(trimmed)) {
return trimmed
.replace("https://preprod.octopusdesign.fr/api-octopus/server", PRODUCTION_ORIGIN)
.replace("https://preprod.octopusdesign.fr", PRODUCTION_ORIGIN)
.replace("http://127.0.0.1", PRODUCTION_ORIGIN)
.replace("http://localhost", PRODUCTION_ORIGIN);
}
return value;
}
};
const normalizeAttributeUrls = (attributes) => {
if (!attributes) return attributes;
const next = { ...attributes };
if (shouldNormalize(next.href)) {
next.href = normalizeToProduction(next.href);
}
if (shouldNormalize(next.content)) {
next.content = normalizeToProduction(next.content);
}
return next;
};
const SEO = ({
pageUrl,
defaultTitle = "Titre par défaut",
@@ -18,18 +98,29 @@ const SEO = ({
const [rankMathHead, setRankMathHead] = useState(null);
const [shouldFallback, setShouldFallback] = useState(true);
const resolvedCanonical = defaultCanonicalUrl || getWindowHref();
const targetUrl = pageUrl || resolvedCanonical;
const runtimeUrl = getWindowHref();
const rankMathTargetUrl = pageUrl || defaultCanonicalUrl || runtimeUrl;
const normalizedPageUrl = normalizeToProduction(pageUrl);
const resolvedCanonical =
normalizeToProduction(defaultCanonicalUrl) ||
normalizedPageUrl ||
normalizeToProduction(runtimeUrl) ||
PRODUCTION_ORIGIN;
const targetUrl =
normalizedPageUrl ||
resolvedCanonical ||
normalizeToProduction(rankMathTargetUrl) ||
PRODUCTION_ORIGIN;
useEffect(() => {
if (!targetUrl) return;
if (!rankMathTargetUrl) return;
const controller = new AbortController();
const fetchRankMathHead = async () => {
try {
const { data } = await api.get("rankmath/v1/getHead", {
params: { url: targetUrl },
params: { url: rankMathTargetUrl },
signal: controller.signal,
});
@@ -53,10 +144,10 @@ const SEO = ({
}, {});
const metaTags = Array.from(headElement.querySelectorAll("meta")).map(
(meta) => attributesToObject(meta)
(meta) => normalizeAttributeUrls(attributesToObject(meta))
);
const linkTags = Array.from(headElement.querySelectorAll("link")).map(
(link) => attributesToObject(link)
(link) => normalizeAttributeUrls(attributesToObject(link))
);
const scriptTags = Array.from(
headElement.querySelectorAll("script")
@@ -96,7 +187,37 @@ const SEO = ({
const rankMathNodes = useMemo(() => {
if (!rankMathHead) return null;
const renderMetaTags = rankMathHead.meta.map((attributes, index) => (
const sanitizeMeta = (meta = []) => {
const filtered = meta.filter((attributes) => {
const name = attributes?.name?.toLowerCase();
if (name === "robots") {
if (FORCE_NO_INDEX) {
return false;
}
const content = attributes.content?.toLowerCase() || "";
if (content.includes("noindex") || content.includes("nofollow")) {
return false;
}
}
return true;
});
const hasRobotsTag = filtered.some(
(attributes) => attributes?.name?.toLowerCase() === "robots"
);
const withRobots = FORCE_NO_INDEX
? [...filtered, { name: "robots", content: DEFAULT_ROBOTS }]
: hasRobotsTag
? filtered
: [...filtered, { name: "robots", content: DEFAULT_ROBOTS }];
return withRobots.map((attributes) => normalizeAttributeUrls(attributes));
};
const sanitizedMeta = sanitizeMeta(rankMathHead.meta);
const renderMetaTags = sanitizedMeta.map((attributes, index) => (
<meta key={`rank-math-meta-${index}`} {...attributes} />
));
@@ -130,6 +251,7 @@ const SEO = ({
<>
<title>{defaultTitle}</title>
<meta name="description" content={defaultDescription} />
<meta name="robots" content={DEFAULT_ROBOTS} />
{resolvedCanonical && (
<link rel="canonical" href={resolvedCanonical} />
)}