Version stable, aprelic, connexion CFRS et autres corrections

This commit is contained in:
Oleg Barykin committed 2026-04-17 20:18:11 +02:00
1 parent 865de8a976
commit e9d0010451
62 files changed
+17483 -7256

No files matched your search

+292 -69
View File
@@ -5,8 +5,100 @@
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
require_once __DIR__ . '/includes/search_helpers.php';
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
// Redirection de la recherche libre vers URL SEO
if (
($currentPath === '/recherche' || $currentPath === '/recherche/' || $currentPath === '/result_search.php')
&& empty($_GET['ajax'])
) {
$qsSpecialite = trim((string) ($_GET['specialite'] ?? ''));
$qsVille = trim((string) ($_GET['ville'] ?? ''));
if ($qsSpecialite !== '' || $qsVille !== '') {
$target = '/recherche';
$specSlug = '';
if ($qsSpecialite === '__OTHERS__') {
$specSlug = 'autres-specialistes';
} elseif ($qsSpecialite !== '') {
$specSlug = seo_spec_slug_from_input($qsSpecialite) ?? '';
}
$citySlug = '';
if ($qsVille !== '') {
$citySlug = seo_city_slug_from_input($qsVille);
}
if ($specSlug !== '') {
$target .= '/' . rawurlencode($specSlug);
}
if ($specSlug !== '') {
if ($citySlug !== '') {
$target .= '/' . rawurlencode($citySlug);
}
$target .= '/';
$extra = $_GET;
unset($extra['specialite'], $extra['ville'], $extra['slug']);
if (! empty($extra)) {
$target .= '?' . http_build_query($extra);
}
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
header('Location: ' . $target, true, 301);
exit;
}
} elseif ($citySlug !== '') {
$target = '/recherche/ville/' . rawurlencode($citySlug) . '/';
$extra = $_GET;
unset($extra['specialite'], $extra['ville'], $extra['slug']);
if (! empty($extra)) {
$target .= '?' . http_build_query($extra);
}
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
header('Location: ' . $target, true, 301);
exit;
}
}
}
}
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
$canon = $SPEC_CANON[$lang] ?? [];
//Ajout 1 pourpraticien_traduction
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
// Lecture et normalisation des filtres
$specialiteParam = trim($_GET['specialite'] ?? '');
$ville = trim($_GET['ville'] ?? '');
// 🔑 On garde le slug SEO ici
$resolvedSeoSpecSlug = null;
// Remap de la spécialité URL/texte vers la valeur de recherche
if ($specialiteParam !== '' && $specialiteParam !== '__OTHERS__') {
$seoSpecSlug = seo_spec_slug_from_input($specialiteParam);
if ($seoSpecSlug !== null) {
$resolvedSeoSpecSlug = $seoSpecSlug; // ⭐ IMPORTANT
$searchSpec = seo_spec_search_value_from_slug($seoSpecSlug, $lang);
if ($searchSpec !== null) {
$specialiteParam = $searchSpec;
}
}
}
$H1_LABELS = [
'ru' => [
@@ -46,25 +138,21 @@
$u = trim($url);
// YouTube
if (preg_match('~(?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];
return ['provider' => 'youtube', 'id' => $id, 'thumb' => "https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
// Dailymotion
if (preg_match('~dailymotion\.com/video/([A-Za-z0-9]+)|dai\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];
return ['provider' => 'dailymotion', 'id' => $id, 'thumb' => "https://www.dailymotion.com/thumbnail/video/$id"];
}
// Direct file (mp4/webm/ogg) — no reliable thumb
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
return ['provider' => 'file', 'id' => null, 'thumb' => null];
}
// Vimeo and others — fallback placeholder
if (strpos(strtolower($u), 'vimeo.com') !== false) {
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
}
@@ -72,12 +160,8 @@
return ['provider' => 'other', 'id' => null, 'thumb' => null];
}
// Lecture et normalisation des filtres
$specialiteParam = trim($_GET['specialite'] ?? '');
$ville = trim($_GET['ville'] ?? '');
// inverse map: canon_value -> key (avocats, vrachi, ...)
$canonToKey = [];
// inverse map: canon_value -> key
$canonToKey = [];
foreach (($SPEC_CANON[$lang] ?? []) as $k => $v) {
$canonToKey[mb_strtolower($v, 'UTF-8')] = $k;
}
@@ -86,7 +170,7 @@
$h1Default = $translations['res_poisk'] ?? 'Результаты поиска';
$h1 = $h1Default;
// On normalise l'affichage du terme (pour l'utiliser plus tard si besoin)
// Normalisation affichage terme
$sp = trim(str_replace(["\xC2\xA0"], ' ', $specialiteParam));
$sp = preg_replace('/\s+/u', ' ', $sp);
$sp_l = mb_strtolower($sp, 'UTF-8');
@@ -94,14 +178,33 @@
if ($specialiteParam === '__OTHERS__') {
$label = $H1_LABELS[$lang]['drugie'] ?? $h1Default;
$h1 = $label . $inFrance;
} elseif (! empty($resolvedSeoSpecSlug)) {
$slugToKey = [
'avocat' => 'avocats',
'consultant' => 'consultants',
'medecin' => 'vrachi',
'psychologue' => 'psychologues',
'professeur' => 'repetitori',
'comptable' => 'finance',
'traducteur' => 'perevod',
'notaire' => 'notarius',
'guide' => 'gid',
'agent-immobilier' => 'rieltori',
'autres-specialistes' => 'drugie',
];
$h1Key = $slugToKey[$resolvedSeoSpecSlug] ?? null;
if ($h1Key !== null) {
$label = $H1_LABELS[$lang][$h1Key] ?? $h1Default;
$h1 = $label . $inFrance;
}
} elseif ($sp !== '' && isset($canonToKey[$sp_l])) {
$key = $canonToKey[$sp_l];
$label = $H1_LABELS[$lang][$key] ?? ($translations[$key] ?? $sp);
$h1 = $label . $inFrance;
}
// sinon: on laisse $h1Default, on décidera après la requête SQL (selon résultats)
// Cas spécial : "Другие специалисты" (chip "drugie")
// Cas spécial : autres spécialistes
$onlyOthers = false;
$specialite = $specialiteParam;
@@ -110,7 +213,7 @@
$specialite = '';
}
// Pagination simple
// Pagination
$perPage = 12;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
@@ -119,36 +222,36 @@
$where = [];
$params = [];
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$where[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*=======================================================================================*/
/*========================== BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$where[] = "(p.date_expiration IS NOT NULL AND p.date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*========================================================================================*/
// 🔎 Unifie: le champ "specialite" peut contenir une spécialité OU un nom/prénom
$term = trim((string) $specialite);
$term_l = mb_strtolower($term, 'UTF-8');
// Le champ "specialite" peut contenir une spécialité OU un nom/prénom
$term = trim((string) $specialite);
$term_l = mb_strtolower($term, 'UTF-8');
// 1) Cas "Другие": tout sauf les grandes catégories
// 1) Cas "Другие"
if ($onlyOthers) {
$mainSpecs = array_values($canon);
if ($mainSpecs) {
$ors = [];
foreach ($mainSpecs as $i => $spc) {
$key = ":cat{$i}";
$ors[] = "LOWER(specialite) LIKE $key";
$ors[] = "LOWER(p.specialite) LIKE $key";
$params[$key] = mb_strtolower($spc, 'UTF-8') . '%';
}
$where[] = '(specialite IS NULL OR specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
$where[] = '(p.specialite IS NULL OR p.specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
}
}
// 2) Cas normal : recherche par spécialité / nom / prénom
// 2) Cas normal
elseif ($term !== '') {
$where[] =
"("
. "LOWER(specialite) LIKE :sp_like "
. "OR LOWER(nom) LIKE :nm_prefix1 "
. "OR LOWER(prenom) LIKE :nm_prefix2 "
. "OR LOWER(CONCAT(prenom,' ',nom)) LIKE :nm_like_full1 "
. "OR LOWER(CONCAT(nom,' ',prenom)) LIKE :nm_like_full2 "
. "LOWER(COALESCE(pt.specialite, p.specialite)) LIKE :sp_like "
. "OR LOWER(COALESCE(pt.nom_affiche, p.nom)) LIKE :nm_prefix1 "
. "OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) LIKE :nm_prefix2 "
. "OR LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom),' ',COALESCE(pt.nom_affiche, p.nom))) LIKE :nm_like_full1 "
. "OR LOWER(CONCAT(COALESCE(pt.nom_affiche, p.nom),' ',COALESCE(pt.prenom_affiche, p.prenom))) LIKE :nm_like_full2 "
. ")";
$params[':sp_like'] = '%' . $term_l . '%';
$params[':nm_prefix1'] = $term_l . '%';
@@ -157,7 +260,7 @@
$params[':nm_like_full2'] = '%' . $term_l . '%';
}
// Filtre ville — support latin + cyrillique (alias + translit)
// Filtre ville — support latin + cyrillique
if ($ville !== '') {
$villeCands = city_query_candidates($ville);
$villeCands[] = mb_strtolower($ville, 'UTF-8');
@@ -167,7 +270,7 @@
foreach ($villeCands as $i => $cand) {
$key = ":ville{$i}";
$params[$key] = $cand . '%';
$villeOrs[] = "ville LIKE {$key}";
$villeOrs[] = "LOWER(COALESCE(pt.ville_affichee, p.ville)) LIKE {$key}";
}
if ($villeOrs) {
$where[] = '(' . implode(' OR ', $villeOrs) . ')';
@@ -177,25 +280,54 @@
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
// total
$sqlCount = "SELECT COUNT(*) FROM praticien $whereSql";
$stmt = $pdo->prepare($sqlCount);
$stmt->execute($params);
$sqlCount = "
SELECT COUNT(*)
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
$whereSql
";
$stmt = $pdo->prepare($sqlCount);
$paramsCount = $params;
$paramsCount[':lang'] = $currentLang;
$stmt->execute($paramsCount);
$total = (int) $stmt->fetchColumn();
$pages = max(1, (int) ceil($total / $perPage));
// data
$sql = "SELECT id, nom, prenom, specialite, ville, photo, presentation, video
FROM praticien
$whereSql";
$sql = "
SELECT
p.id,
p.nom,
p.prenom,
p.ville,
p.photo,
p.video,
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
COALESCE(pt.presentation, p.presentation) AS presentation_affichee
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
$whereSql
";
if ($term !== '') {
$sql .= " ORDER BY
(LOWER(CONCAT(prenom,' ',nom)) = :nm_exact1
OR LOWER(CONCAT(nom,' ',prenom)) = :nm_exact2) DESC,
(LOWER(nom) = :nm_exact3 OR LOWER(prenom) = :nm_exact4) DESC,
(LOWER(nom) LIKE :nm_prefix3 OR LOWER(prenom) LIKE :nm_prefix4) DESC,
(LOWER(specialite) LIKE :sp_prefix1) DESC,
nom ASC, prenom ASC";
(LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom),' ',COALESCE(pt.nom_affiche, p.nom))) = :nm_exact1
OR LOWER(CONCAT(COALESCE(pt.nom_affiche, p.nom),' ',COALESCE(pt.prenom_affiche, p.prenom))) = :nm_exact2) DESC,
(LOWER(COALESCE(pt.nom_affiche, p.nom)) = :nm_exact3 OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) = :nm_exact4) DESC,
(LOWER(COALESCE(pt.nom_affiche, p.nom)) LIKE :nm_prefix3 OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) LIKE :nm_prefix4) DESC,
(LOWER(COALESCE(pt.specialite, p.specialite)) LIKE :sp_prefix1) DESC,
COALESCE(pt.nom_affiche, p.nom) ASC,
COALESCE(pt.prenom_affiche, p.prenom) ASC";
$params[':nm_exact1'] = $term_l;
$params[':nm_exact2'] = $term_l;
$params[':nm_exact3'] = $term_l;
@@ -204,14 +336,15 @@
$params[':nm_prefix4'] = $term_l . '%';
$params[':sp_prefix1'] = $term_l . '%';
} else {
$sql .= " ORDER BY updated_at DESC, id DESC";
$sql .= " ORDER BY p.updated_at DESC, p.id DESC";
}
$sql .= " LIMIT :offset, :limit";
$stmt = $pdo->prepare($sql);
// Lie tous les :villeN / :specialite
$params[':lang'] = $currentLang;
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v, PDO::PARAM_STR);
}
@@ -221,15 +354,16 @@
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ✅ Si ce n'est pas une catégorie canon / pas __OTHERS__
// et qu'on a des résultats => on affiche "Résultats pour : X"
if ($h1 === $h1Default && $specialiteParam !== '__OTHERS__' && $sp !== '' && ! isset($canonToKey[$sp_l])) {
// Si ce n'est pas une catégorie canonique et qu'on a des résultats
if (
$h1 === $h1Default
&& $specialiteParam !== '__OTHERS__'
&& $sp !== ''
&& empty($resolvedSeoSpecSlug)
&& ! isset($canonToKey[$sp_l])
) {
if (! empty($rows)) {
if ($lang === 'fr') {
$h1 = ''; //'Résultats de recherche pour : ' . $sp;
} else {
$h1 = ''; //'Результаты поиска по: ' . $sp;
}
$h1 = '';
}
}
@@ -263,15 +397,103 @@
}
$baseQuery = http_build_query($qParams);
// =========================
// SEO dynamique
// =========================
$seoSpecialite = trim((string) $specialiteParam);
$seoVille = trim((string) $ville);
$isCanonCategory = ($seoSpecialite !== '' && isset($canonToKey[mb_strtolower($seoSpecialite, 'UTF-8')]));
$isOthersCategory = ($seoSpecialite === '__OTHERS__');
$seoDisplaySpecialite = $seoSpecialite;
if ($isOthersCategory) {
$seoDisplaySpecialite = $translations['drugie'] ?? 'Spécialistes';
}
if ($seoDisplaySpecialite === '') {
$seoTitle = $translations['boostseo_search_title_default'] ?? '';
$seoDesc = $translations['boostseo_search_desc_default'] ?? '';
$seoIntro = $translations['boostseo_search_intro_default'] ?? '';
} elseif ($seoVille !== '') {
$seoTitle = strtr(($translations['boostseo_search_title_spec_city'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
'%ville%' => $seoVille,
]);
$seoDesc = strtr(($translations['boostseo_search_desc_spec_city'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
'%ville%' => $seoVille,
]);
$seoIntro = strtr(($translations['boostseo_search_intro_spec_city'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
'%ville%' => $seoVille,
]);
} else {
$seoTitle = strtr(($translations['boostseo_search_title_spec'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
]);
$seoDesc = strtr(($translations['boostseo_search_desc_spec'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
]);
$seoIntro = strtr(($translations['boostseo_search_intro_spec'] ?? ''), [
'%specialite%' => $seoDisplaySpecialite,
]);
}
$canonicalUrl = 'https://uslugi.fr/recherche';
$canonicalSpecSlug = '';
if ($specialiteParam === '__OTHERS__') {
$canonicalSpecSlug = 'autres-specialistes';
} elseif ($specialiteParam !== '') {
foreach (seo_spec_map() as $slug => $cfg) {
if (($cfg['db'] ?? '') === $specialiteParam) {
$canonicalSpecSlug = $slug;
break;
}
}
}
$cityCanonicalSlug = '';
if ($ville !== '') {
$cityCanonicalSlug = seo_city_slug_from_input($ville);
}
if ($canonicalSpecSlug !== '') {
$canonicalUrl .= '/' . rawurlencode($canonicalSpecSlug);
if ($cityCanonicalSlug !== '') {
$canonicalUrl .= '/' . rawurlencode($cityCanonicalSlug);
}
} elseif ($cityCanonicalSlug !== '') {
$canonicalUrl .= '/ville/' . rawurlencode($cityCanonicalSlug);
}
$canonicalUrl .= '/';
if ($page > 1) {
$canonicalUrl .= '?page=' . $page;
}
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title><?php echo htmlspecialchars($translations['seo_result_search_title'] ?? '', ENT_QUOTES, 'UTF-8') ?></title>
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_result_search_desc'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_result_search_keywords'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<meta name="author" content="Uslugi.fr">
<title><?php echo h($seoTitle) ?></title>
<meta name="description" content="<?php echo h($seoDesc) ?>">
<meta name="author" content="Uslugi.fr">
<meta name="robots" content="index, follow">
<link rel="canonical" href="<?php echo h($canonicalUrl) ?>">
<link rel="alternate" hreflang="ru" href="<?php echo h($canonicalUrl . (str_contains($canonicalUrl, '?') ? '&lang=ru' : '?lang=ru')) ?>">
<link rel="alternate" hreflang="fr" href="<?php echo h($canonicalUrl . (str_contains($canonicalUrl, '?') ? '&lang=fr' : '?lang=fr')) ?>">
<link rel="alternate" hreflang="x-default" href="<?php echo h($canonicalUrl) ?>">
<meta property="og:title" content="<?php echo h($seoTitle) ?>">
<meta property="og:description" content="<?php echo h($seoDesc) ?>">
<meta property="og:type" content="website">
<meta property="og:url" content="<?php echo h($canonicalUrl) ?>">
<meta property="og:image" content="https://uslugi.fr/public/uploads/img/logo.png">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/css/bootstrap.min.css" rel="stylesheet">
@@ -296,8 +518,8 @@ else: ?>
<div class="container">
<div class="header-left">
<div class="brand">
<a href="index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
<a href="index.php"><img src="/public/uploads/img/logo.png" alt="uslugi.fr" class="logo-desktop"></a>
<a href="/index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
<a href="/index.php"><img src="/public/uploads/img/logo.png" alt="uslugi.fr" class="logo-desktop"></a>
</div>
<div class="lang-switch">
<?php require __DIR__ . '/includes/menu_langue.php'; ?>
@@ -330,6 +552,7 @@ else: ?>
<div class="content">
<div class="container">
<h1 class="results-title"><?php echo h($h1) ?></h1>
<p class="visually-hidden"><?php echo h($seoIntro) ?></p>
</div>
<section class="cards-section">
@@ -342,25 +565,25 @@ else: ?>
?>
<article class="pro-card">
<div class="pro-photo">
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="Фото специалиста">
</a>
</div>
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="<?php echo h(trim(($p['prenom_affiche'] ?? '') . ' ' . ($p['nom_affiche'] ?? '') . ' — ' . ($p['specialite_affichee'] ?? '') . (! empty($p['ville_affichee']) ? ' — ' . $p['ville_affichee'] : ''))) ?>">
</a>
</div>
<div class="pro-info">
<h3 class="pro-name">
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<?php echo h(($p['prenom'] ?? '') . ' ' . ($p['nom'] ?? '')) ?>
<?php echo h(($p['prenom_affiche'] ?? '') . ' ' . ($p['nom_affiche'] ?? '')) ?>
</a>
</h3>
<?php if (! empty($p['specialite'])): ?>
<div class="pro-spec"><?php echo h($p['specialite']) ?></div>
<?php if (! empty($p['specialite_affichee'])): ?>
<div class="pro-spec"><?php echo h($p['specialite_affichee']) ?></div>
<?php endif; ?>
<?php if (! empty($p['presentation'])): ?>
<p class="pro-desc"><?php echo h(excerpt($p['presentation'], 360)) ?></p>
<?php endif; ?>
<?php if (! empty($p['presentation_affichee'])): ?>
<p class="pro-desc"><?php echo h(excerpt($p['presentation_affichee'], 360)) ?></p>
<?php endif; ?>
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<?php echo h($translations['plus_details'] ?? 'Подробнее'); ?>