mise a jour structure SEO
This commit is contained in:
@@ -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
@@ -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} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user