mise a jour structure SEO
This commit is contained in:
@@ -49,6 +49,53 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
.no-js-wrapper {
|
||||||
|
padding: 1.5rem;
|
||||||
|
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #f5f7fb;
|
||||||
|
color: #0b1f44;
|
||||||
|
}
|
||||||
|
.no-js-wrapper a {
|
||||||
|
color: #0b5ed7;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.no-js-wrapper a:hover,
|
||||||
|
.no-js-wrapper a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.no-js-nav {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<section class="no-js-wrapper">
|
||||||
|
<h2>Navigation sans JavaScript</h2>
|
||||||
|
<p>
|
||||||
|
Le site s'appuie sur React et nécessite JavaScript pour l'expérience complète.
|
||||||
|
Activez-le pour profiter de toutes les fonctionnalités. En attendant,
|
||||||
|
utilisez ces liens directs :
|
||||||
|
</p>
|
||||||
|
<ul class="no-js-nav">
|
||||||
|
<li><a href="/">Accueil</a></li>
|
||||||
|
<li><a href="/services/prestation-maitrise-oeuvre/">Création de site web</a></li>
|
||||||
|
<li><a href="/services/formations-web/">Formations web</a></li>
|
||||||
|
<li><a href="/services/electricite/">Graphisme</a></li>
|
||||||
|
<li><a href="/services/service-securite-incendie/">Podcast</a></li>
|
||||||
|
<li><a href="/services/expertises-tce/">Formations professionnelles</a></li>
|
||||||
|
<li><a href="/nosFormations/">Nos formations</a></li>
|
||||||
|
<li><a href="/posts/">Actualités</a></li>
|
||||||
|
<li><a href="/contact/">Contact</a></li>
|
||||||
|
<li><a href="/search/">Recherche</a></li>
|
||||||
|
<li><a href="/sitemap.xml">Plan du site complet</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Retrouvez aussi nos informations légales et politiques via le plan du site.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</noscript>
|
||||||
<script type="module" src="/src/main.jsx" defer></script>
|
<script type="module" src="/src/main.jsx" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+754
-4
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,12 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"prebuild": "node scripts/update-noscript.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"postbuild": "node scripts/prerender.mjs",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"prerender": "node scripts/prerender.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@@ -41,6 +44,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
|
"puppeteer": "^24.28.0",
|
||||||
"rollup-plugin-visualizer": "^5.14.0",
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.0.5",
|
||||||
"vite-plugin-compression": "^0.5.1"
|
"vite-plugin-compression": "^0.5.1"
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /static/
|
Disallow: /
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://octopusdesign.fr/sitemap.xml
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { createServer } from "node:http";
|
||||||
|
import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import puppeteer from "puppeteer";
|
||||||
|
import { PUBLIC_ROUTES, NOT_FOUND_ROUTE } from "./routes.mjs";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const distDir = path.resolve(__dirname, "../dist");
|
||||||
|
|
||||||
|
const routes = PUBLIC_ROUTES.filter((route) => route.prerender).map(
|
||||||
|
(route) => route.path
|
||||||
|
);
|
||||||
|
|
||||||
|
const mimeTypes = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "application/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".json": "application/json",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".avif": "image/avif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".txt": "text/plain",
|
||||||
|
".xml": "application/xml",
|
||||||
|
".map": "application/json",
|
||||||
|
".gz": "application/gzip",
|
||||||
|
".br": "application/brotli",
|
||||||
|
};
|
||||||
|
|
||||||
|
function serveStaticFile(res, filePath) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const contentType = mimeTypes[ext] ?? "application/octet-stream";
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader("Content-Type", contentType);
|
||||||
|
return readFile(filePath).then((data) => res.end(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStaticServer() {
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
||||||
|
let pathname = decodeURIComponent(requestUrl.pathname);
|
||||||
|
const hasExtension = path.extname(pathname) !== "";
|
||||||
|
|
||||||
|
if (pathname.endsWith("/")) {
|
||||||
|
pathname = pathname.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath = path.join(distDir, pathname);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileStat = await stat(filePath);
|
||||||
|
if (fileStat.isDirectory()) {
|
||||||
|
filePath = path.join(filePath, "index.html");
|
||||||
|
}
|
||||||
|
await serveStaticFile(res, filePath);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (hasExtension) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filePath = path.join(distDir, "index.html");
|
||||||
|
await serveStaticFile(res, filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(`Server error: ${error instanceof Error ? error.message : error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
server.listen(0, () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (address && typeof address === "object") {
|
||||||
|
resolve({ server, port: address.port });
|
||||||
|
} else {
|
||||||
|
resolve({ server, port: 4173 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
async function renderRoute(browser, baseUrl, route, outputFile) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const targetUrl = `${baseUrl}${route}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(targetUrl, {
|
||||||
|
waitUntil: ["networkidle0", "domcontentloaded"],
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`[prerender] Navigation to ${targetUrl} failed: ${
|
||||||
|
error instanceof Error ? error.message : error
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Donne un léger délai pour laisser React terminer le rendu asynchrone
|
||||||
|
await delay(1500);
|
||||||
|
const html = await page.content();
|
||||||
|
const outputPath =
|
||||||
|
outputFile ??
|
||||||
|
(route === "/"
|
||||||
|
? path.join(distDir, "index.html")
|
||||||
|
: path.join(distDir, route.replace(/^\//, ""), "index.html"));
|
||||||
|
|
||||||
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||||
|
await writeFile(outputPath, html, "utf8");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✔ prerendered ${route} -> ${path.relative(distDir, outputPath) || "index.html"}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { server, port } = await createStaticServer();
|
||||||
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||||||
|
const browser = await puppeteer.launch({ headless: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const route of routes) {
|
||||||
|
await renderRoute(browser, baseUrl, route);
|
||||||
|
}
|
||||||
|
await renderRoute(browser, baseUrl, NOT_FOUND_ROUTE, path.join(distDir, "404.html"));
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("[prerender] Failed to prerender routes:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const withTrailingSlash = (path) => {
|
||||||
|
if (!path || path === "/") return "/";
|
||||||
|
return path.endsWith("/") ? path : `${path}/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUBLIC_ROUTES = [
|
||||||
|
{ path: "/", href: "/", label: "Accueil", prerender: true },
|
||||||
|
{
|
||||||
|
path: "/services/prestation-maitrise-oeuvre",
|
||||||
|
href: withTrailingSlash("/services/prestation-maitrise-oeuvre"),
|
||||||
|
label: "Création de site web",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/services/formations-web",
|
||||||
|
href: withTrailingSlash("/services/formations-web"),
|
||||||
|
label: "Formations web",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/services/electricite",
|
||||||
|
href: withTrailingSlash("/services/electricite"),
|
||||||
|
label: "Graphisme",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/services/service-securite-incendie",
|
||||||
|
href: withTrailingSlash("/services/service-securite-incendie"),
|
||||||
|
label: "Podcast",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/services/expertises-tce",
|
||||||
|
href: withTrailingSlash("/services/expertises-tce"),
|
||||||
|
label: "Formations professionnelles",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/nosFormations",
|
||||||
|
href: withTrailingSlash("/nosFormations"),
|
||||||
|
label: "Nos formations",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/posts",
|
||||||
|
href: withTrailingSlash("/posts"),
|
||||||
|
label: "Actualités",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/contact",
|
||||||
|
href: withTrailingSlash("/contact"),
|
||||||
|
label: "Contact",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/search",
|
||||||
|
href: withTrailingSlash("/search"),
|
||||||
|
label: "Recherche",
|
||||||
|
prerender: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/sitemap.xml",
|
||||||
|
href: "/sitemap.xml",
|
||||||
|
label: "Plan du site complet",
|
||||||
|
prerender: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NOT_FOUND_ROUTE = "/__404";
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { PUBLIC_ROUTES } from "./routes.mjs";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const indexPath = path.resolve(__dirname, "../index.html");
|
||||||
|
|
||||||
|
const LIST_INDENT = " ";
|
||||||
|
const WRAPPER_INDENT = " ";
|
||||||
|
|
||||||
|
const generateListItems = () =>
|
||||||
|
PUBLIC_ROUTES.map(
|
||||||
|
({ path, href, label }) => {
|
||||||
|
const targetHref = href ?? path;
|
||||||
|
return `${LIST_INDENT}<li><a href="${targetHref}">${label}</a></li>`;
|
||||||
|
}
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const originalContent = await readFile(indexPath, "utf8");
|
||||||
|
|
||||||
|
const navPattern = new RegExp(
|
||||||
|
`${WRAPPER_INDENT}<ul class="no-js-nav">[\\s\\S]*?${WRAPPER_INDENT}<\\/ul>`,
|
||||||
|
"m"
|
||||||
|
);
|
||||||
|
const match = originalContent.match(navPattern);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(
|
||||||
|
"Bloc <ul class=\"no-js-nav\"> introuvable dans index.html. Impossible de mettre à jour la navigation noscript."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacement = `${WRAPPER_INDENT}<ul class="no-js-nav">\n${generateListItems()}\n${WRAPPER_INDENT}</ul>`;
|
||||||
|
const updatedContent = originalContent.replace(navPattern, replacement);
|
||||||
|
|
||||||
|
if (updatedContent !== originalContent) {
|
||||||
|
await writeFile(indexPath, updatedContent, "utf8");
|
||||||
|
console.log("✔ Mise à jour de la navigation noscript dans index.html");
|
||||||
|
} else {
|
||||||
|
console.log("ℹ Navigation noscript déjà à jour");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("[update-noscript] Échec de la mise à jour :", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import { Box, Typography, CircularProgress } from "@mui/material";
|
|||||||
const API_URL =
|
const API_URL =
|
||||||
"https://www.formations.octopusdesign.fr/webservice/rest/server.php";
|
"https://www.formations.octopusdesign.fr/webservice/rest/server.php";
|
||||||
const TOKEN = "685e1b5d794b558b60e971581154c3b2";
|
const TOKEN = "685e1b5d794b558b60e971581154c3b2";
|
||||||
|
const PUBLIC_LINKS_STORAGE_KEY = "listeCoursPublicLinks";
|
||||||
|
|
||||||
const CoursLecture = () => {
|
const CoursLecture = () => {
|
||||||
// Empêche le clic droit
|
// Empêche le clic droit
|
||||||
@@ -23,6 +24,7 @@ const CoursLecture = () => {
|
|||||||
|
|
||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
const [content, setContent] = useState(null);
|
const [content, setContent] = useState(null);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [tokenValid, setTokenValid] = useState(true);
|
const [tokenValid, setTokenValid] = useState(true);
|
||||||
|
|
||||||
@@ -46,15 +48,131 @@ const CoursLecture = () => {
|
|||||||
|
|
||||||
// ✅ Récupération contenu
|
// ✅ Récupération contenu
|
||||||
useEffect(() => {
|
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);
|
setTokenValid(false);
|
||||||
setContent("⛔ Le cours est terminé !");
|
setContent("⛔ Le cours est terminé !");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
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 () => {
|
const fetchPage = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(API_URL, {
|
const res = await axios.get(API_URL, {
|
||||||
@@ -62,7 +180,7 @@ const CoursLecture = () => {
|
|||||||
wstoken: TOKEN,
|
wstoken: TOKEN,
|
||||||
wsfunction: "mod_page_get_pages_by_courses",
|
wsfunction: "mod_page_get_pages_by_courses",
|
||||||
moodlewsrestformat: "json",
|
moodlewsrestformat: "json",
|
||||||
courseids: [parseInt(courseId)],
|
courseids: [parseInt(courseId, 10)],
|
||||||
},
|
},
|
||||||
paramsSerializer: (params) => {
|
paramsSerializer: (params) => {
|
||||||
const sp = new URLSearchParams();
|
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) {
|
if (found) {
|
||||||
setContent(found.content);
|
setTitle(found.name || "");
|
||||||
|
setContent(found.content || "");
|
||||||
} else {
|
} else {
|
||||||
setTokenValid(false);
|
setTokenValid(false);
|
||||||
setContent("⛔ Page non trouvée.");
|
setContent("⛔ Page non trouvée.");
|
||||||
@@ -92,7 +213,43 @@ const CoursLecture = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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();
|
fetchPage();
|
||||||
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -140,6 +297,11 @@ const CoursLecture = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: 4, mt: 5 }}>
|
<Box sx={{ padding: 4, mt: 5 }}>
|
||||||
|
{title ? (
|
||||||
|
<Typography variant="h4" sx={{ mb: 3, color: "#315397" }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||||
</Box>
|
</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 { Helmet } from "react-helmet-async";
|
||||||
import api from "../api";
|
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 =
|
const DEFAULT_OG_IMAGE =
|
||||||
"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";
|
||||||
|
const DEFAULT_ROBOTS = FORCE_NO_INDEX ? "noindex, nofollow" : "index, follow";
|
||||||
|
|
||||||
const getWindowHref = () =>
|
const getWindowHref = () =>
|
||||||
typeof window !== "undefined" ? window.location.href : "";
|
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 = ({
|
const SEO = ({
|
||||||
pageUrl,
|
pageUrl,
|
||||||
defaultTitle = "Titre par défaut",
|
defaultTitle = "Titre par défaut",
|
||||||
@@ -18,18 +98,29 @@ const SEO = ({
|
|||||||
const [rankMathHead, setRankMathHead] = useState(null);
|
const [rankMathHead, setRankMathHead] = useState(null);
|
||||||
const [shouldFallback, setShouldFallback] = useState(true);
|
const [shouldFallback, setShouldFallback] = useState(true);
|
||||||
|
|
||||||
const resolvedCanonical = defaultCanonicalUrl || getWindowHref();
|
const runtimeUrl = getWindowHref();
|
||||||
const targetUrl = pageUrl || resolvedCanonical;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!targetUrl) return;
|
if (!rankMathTargetUrl) return;
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const fetchRankMathHead = async () => {
|
const fetchRankMathHead = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get("rankmath/v1/getHead", {
|
const { data } = await api.get("rankmath/v1/getHead", {
|
||||||
params: { url: targetUrl },
|
params: { url: rankMathTargetUrl },
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,10 +144,10 @@ const SEO = ({
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const metaTags = Array.from(headElement.querySelectorAll("meta")).map(
|
const metaTags = Array.from(headElement.querySelectorAll("meta")).map(
|
||||||
(meta) => attributesToObject(meta)
|
(meta) => normalizeAttributeUrls(attributesToObject(meta))
|
||||||
);
|
);
|
||||||
const linkTags = Array.from(headElement.querySelectorAll("link")).map(
|
const linkTags = Array.from(headElement.querySelectorAll("link")).map(
|
||||||
(link) => attributesToObject(link)
|
(link) => normalizeAttributeUrls(attributesToObject(link))
|
||||||
);
|
);
|
||||||
const scriptTags = Array.from(
|
const scriptTags = Array.from(
|
||||||
headElement.querySelectorAll("script")
|
headElement.querySelectorAll("script")
|
||||||
@@ -96,7 +187,37 @@ const SEO = ({
|
|||||||
const rankMathNodes = useMemo(() => {
|
const rankMathNodes = useMemo(() => {
|
||||||
if (!rankMathHead) return null;
|
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} />
|
<meta key={`rank-math-meta-${index}`} {...attributes} />
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -130,6 +251,7 @@ const SEO = ({
|
|||||||
<>
|
<>
|
||||||
<title>{defaultTitle}</title>
|
<title>{defaultTitle}</title>
|
||||||
<meta name="description" content={defaultDescription} />
|
<meta name="description" content={defaultDescription} />
|
||||||
|
<meta name="robots" content={DEFAULT_ROBOTS} />
|
||||||
{resolvedCanonical && (
|
{resolvedCanonical && (
|
||||||
<link rel="canonical" href={resolvedCanonical} />
|
<link rel="canonical" href={resolvedCanonical} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user