mise a jour structure SEO
This commit is contained in:
parent
5909871dec
commit
1890d830e1
@ -49,6 +49,53 @@
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
758
frontend/package-lock.json
generated
758
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,9 +5,12 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"prebuild": "node scripts/update-noscript.mjs",
|
||||
"build": "vite build",
|
||||
"postbuild": "node scripts/prerender.mjs",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"prerender": "node scripts/prerender.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@ -41,6 +44,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.4.49",
|
||||
"puppeteer": "^24.28.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-compression": "^0.5.1"
|
||||
|
||||
@ -1,5 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /static/
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://octopusdesign.fr/sitemap.xml
|
||||
Disallow: /
|
||||
|
||||
149
frontend/scripts/prerender.mjs
Normal file
149
frontend/scripts/prerender.mjs
Normal file
@ -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);
|
||||
});
|
||||
70
frontend/scripts/routes.mjs
Normal file
70
frontend/scripts/routes.mjs
Normal file
@ -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";
|
||||
50
frontend/scripts/update-noscript.mjs
Normal file
50
frontend/scripts/update-noscript.mjs
Normal file
@ -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 =
|
||||
"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} />
|
||||
)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user