723 lines
25 KiB
PHP
723 lines
25 KiB
PHP
<?php
|
|
declare (strict_types = 1);
|
|
|
|
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
|
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
|
require_once __DIR__ . '/includes/search_helpers.php';
|
|
|
|
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
|
$canon = $SPEC_CANON[$lang] ?? [];
|
|
|
|
/* =========================
|
|
AJAX: suggestions
|
|
========================= */
|
|
if (isset($_GET['ajax']) && $_GET['ajax'] === 'suggest') {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$field = (string) ($_GET['field'] ?? '');
|
|
$q = trim((string) ($_GET['q'] ?? ''));
|
|
if (! in_array($field, ['specialite', 'ville'], true)) {echo json_encode([]);exit;}
|
|
|
|
require_once __DIR__ . '/includes/search_helpers.php';
|
|
|
|
$params = [];
|
|
$sql = '';
|
|
|
|
if ($field === 'ville') {
|
|
$sql = "SELECT DISTINCT ville AS val
|
|
FROM praticien
|
|
WHERE ville IS NOT NULL AND ville <> ''";
|
|
|
|
if ($q !== '') {
|
|
$cands = city_query_candidates($q);
|
|
$cands[] = mb_strtolower($q, 'UTF-8');
|
|
$cands = array_values(array_unique($cands));
|
|
|
|
$ors = [];
|
|
foreach ($cands as $i => $cand) {
|
|
$ors[] = "LOWER(ville) LIKE :q$i";
|
|
$params[":q$i"] = $cand . '%';
|
|
}
|
|
if ($ors) {
|
|
$sql .= " AND (" . implode(' OR ', $ors) . ")";
|
|
}
|
|
|
|
}
|
|
$sql .= " ORDER BY val ASC LIMIT 15";
|
|
|
|
} else {
|
|
$sql = "SELECT DISTINCT val FROM (
|
|
SELECT specialite AS val
|
|
FROM praticien
|
|
WHERE specialite IS NOT NULL AND specialite <> ''
|
|
UNION ALL
|
|
SELECT CONCAT(prenom, ' ', nom) AS val
|
|
FROM praticien
|
|
WHERE prenom IS NOT NULL AND prenom <> '' AND nom IS NOT NULL AND nom <> ''
|
|
UNION ALL
|
|
SELECT prenom AS val
|
|
FROM praticien
|
|
WHERE prenom IS NOT NULL AND prenom <> ''
|
|
UNION ALL
|
|
SELECT nom AS val
|
|
FROM praticien
|
|
WHERE nom IS NOT NULL AND nom <> ''
|
|
) t
|
|
WHERE 1";
|
|
|
|
if ($q !== '') {
|
|
$sql .= " AND LOWER(val) LIKE :q";
|
|
$params[':q'] = mb_strtolower($q, 'UTF-8') . '%';
|
|
}
|
|
|
|
$sql .= " ORDER BY (LOWER(val) LIKE :qprefix) DESC, val ASC LIMIT 15";
|
|
$params[':qprefix'] = ($q !== '') ? (mb_strtolower($q, 'UTF-8') . '%') : '';
|
|
}
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
echo json_encode($stmt->fetchAll(PDO::FETCH_COLUMN));
|
|
exit;
|
|
}
|
|
|
|
/* =========================
|
|
Helpers
|
|
========================= */
|
|
function h($v): string
|
|
{return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');}
|
|
$reqUri = h($_SERVER['REQUEST_URI'] ?? '/');
|
|
?>
|
|
<!doctype html>
|
|
<html lang="<?php echo h($lang ?? 'ru') ?>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<title><?php echo h($translations['seo_index_title'] ?? '') ?></title>
|
|
<meta name="description" content="<?php echo h($translations['seo_index_desc'] ?? '') ?>">
|
|
<meta name="keywords" content="<?php echo h($translations['seo_index_keywords'] ?? '') ?>">
|
|
<meta name="author" content="Uslugi.fr">
|
|
<meta name="language" content="<?php echo h($lang ?? 'ru') ?>">
|
|
|
|
<!-- ✅ Canonical + Hreflang (SEO multilingue) -->
|
|
<link rel="canonical" href="https://uslugi.fr<?php echo $reqUri ?>">
|
|
<link rel="alternate" hreflang="ru" href="https://uslugi.fr/ru/">
|
|
<link rel="alternate" hreflang="fr" href="https://uslugi.fr/fr/">
|
|
<link rel="alternate" hreflang="x-default" href="https://uslugi.fr/">
|
|
|
|
<!-- ✅ Open Graph -->
|
|
<meta property="og:title" content="<?php echo h($translations['seo_index_title'] ?? '') ?>">
|
|
<meta property="og:description" content="<?php echo h($translations['seo_index_desc'] ?? '') ?>">
|
|
<meta property="og:image" content="https://uslugi.fr/public/uploads/img/logo_u.png">
|
|
<meta property="og:url" content="https://uslugi.fr<?php echo $reqUri ?>">
|
|
<meta property="og:type" content="website">
|
|
|
|
<!-- PWA -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#38378B">
|
|
|
|
<!-- Favicons -->
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/public/uploads/img/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/public/uploads/img/favicon-16x16.png">
|
|
<link rel="icon" type="image/png" sizes="96x96" href="/public/uploads/img/favicon-96x96.png">
|
|
<link rel="icon" href="/public/uploads/img/favicon.ico" type="image/x-icon">
|
|
<link rel="icon" type="image/svg+xml" href="/public/uploads/img/favicon.svg">
|
|
<meta name="apple-mobile-web-app-title" content="Uslugi.fr">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/public/uploads/img/apple-touch-icon.png">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
|
|
<!-- DNS Prefetch -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
|
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
|
|
|
<!-- CSS -->
|
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
|
<?php if (! empty($logged)): ?>
|
|
<link rel="stylesheet" href="<?php echo h(asset('/includes/site_style.css')) ?>">
|
|
<?php endif; ?>
|
|
<link rel="stylesheet" href="<?php echo h(asset('/includes/custom_style.css')) ?>">
|
|
<link rel="stylesheet" href="<?php echo h(asset('/includes/index_style.css')) ?>">
|
|
<link rel="stylesheet" href="<?php echo h(asset('/includes/mobil_style.css')) ?>">
|
|
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
|
<style>
|
|
.site-header
|
|
{
|
|
background: #FFF;
|
|
border-bottom: 0px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.site-footer
|
|
{
|
|
background: #FFF;
|
|
border-top: 0px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
</style>
|
|
<!-- ✅ Schema (simple, tu peux enrichir plus tard) -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context":"https://schema.org",
|
|
"@type":"LocalBusiness",
|
|
"name":"Uslugi.fr",
|
|
"url":"https://uslugi.fr",
|
|
"logo":"https://uslugi.fr/public/uploads/img/logo.png",
|
|
"address":{"@type":"PostalAddress","addressCountry":"FR"}
|
|
}
|
|
</script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
<?php if (! empty($logged)):
|
|
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
|
else: ?>
|
|
<!-- Header public identique -->
|
|
<header class="site-header">
|
|
<div class="container">
|
|
<div class="header-left">
|
|
<div class="brand">
|
|
<img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile">
|
|
<img src="/public/uploads/img/logo.png" alt="uslugi.fr" class="logo-desktop">
|
|
</div>
|
|
<div class="lang-switch">
|
|
<?php require __DIR__ . '/includes/menu_langue.php'; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="header-actions">
|
|
<a class="btn-login" href="/connexion.php">
|
|
<span class="d-sm-none"><?php echo h($translations['btn_se_connecter'] ?? '') ?></span>
|
|
<span class="d-none d-sm-inline"><?php echo h($translations['ja_spec'] ?? '') ?></span>
|
|
</a>
|
|
|
|
<div class="dropdown">
|
|
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
|
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
|
aria-expanded="false" aria-label="<?php echo h($translations['help'] ?? 'Помощь') ?>">?</button>
|
|
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
|
<li><a href="/how-it-work.php"><?php echo h($translations['kak_rabotaet'] ?? '') ?></a></li>
|
|
<li><a href="/about.php"><?php echo h($translations['pro_nas'] ?? '') ?></a></li>
|
|
<li><a href="/faq.php"><?php echo h($translations['faq'] ?? '') ?></a></li>
|
|
<li><a href="/contact.php"><?php echo h($translations['nam_pisat'] ?? '') ?></a></li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
<?php endif; ?>
|
|
|
|
<!-- HERO -->
|
|
<section class="hero">
|
|
<div class="home-panel" role="region" aria-label="<?php echo h($translations['home_panel_aria'] ?? 'Accueil') ?>">
|
|
<div class="container hero-inner">
|
|
|
|
<!-- PWA Install floating button (mobile only, hidden by default) ✅ -->
|
|
<button id="pwaInstallBtn" class="pwa-install d-none" type="button" aria-label="Installer USLUGI">
|
|
⤓ Uslugi
|
|
</button>
|
|
|
|
<!-- iOS helper (hidden by default) ✅ -->
|
|
<div id="pwaIosHint" class="pwa-ios-hint d-none" role="dialog" aria-modal="true">
|
|
<div class="pwa-ios-card">
|
|
<div class="pwa-ios-title">USLUGI.FR</div>
|
|
<div class="pwa-ios-text">
|
|
На iPhone: нажмите (
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M3.5 6a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-2a.5.5 0 0 1 0-1h2A1.5 1.5 0 0 1 14 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 14.5v-8A1.5 1.5 0 0 1 3.5 5h2a.5.5 0 0 1 0 1z"/>
|
|
<path fill-rule="evenodd" d="M7.646.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 1.707V10.5a.5.5 0 0 1-1 0V1.707L5.354 3.854a.5.5 0 1 1-.708-.708z"/>
|
|
</svg>) <b>Поделиться,</b> затем <b>Добавить на экран</b>.<br><br>
|
|
Sur iPhone : appuie sur <b>Partager</b> (
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M3.5 6a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-2a.5.5 0 0 1 0-1h2A1.5 1.5 0 0 1 14 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 14.5v-8A1.5 1.5 0 0 1 3.5 5h2a.5.5 0 0 1 0 1z"/>
|
|
<path fill-rule="evenodd" d="M7.646.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 1.707V10.5a.5.5 0 0 1-1 0V1.707L5.354 3.854a.5.5 0 1 1-.708-.708z"/>
|
|
</svg>), puis <b>Sur l'écran d'accueil</b>.
|
|
</div>
|
|
<button id="pwaIosClose" class="pwa-ios-close" type="button">OK</button>
|
|
</div>
|
|
</div>
|
|
|
|
<br>
|
|
<h1 class="hero-title">
|
|
<?php echo($translations['titre_home'] ?? '') ?>
|
|
<span class="visually-hidden">
|
|
<?php echo h($translations['boostseo_home_h1_suffix'] ?? '') ?>
|
|
</span>
|
|
</h1>
|
|
<p class="visually-hidden">
|
|
<?php echo h($translations['boostseo_home_intro'] ?? '') ?>
|
|
</p>
|
|
<center><h5><?php echo($translations['sous_titre_home'] ?? '') ?></h5></center>
|
|
<br>
|
|
<?php
|
|
$chipKeysRow1 = ['avocats', 'notarius', 'rieltori'];
|
|
$chipKeysRow2 = ['vrachi', 'psychologues', 'consultants'];
|
|
$chipKeysRow3 = ['perevod', 'repetitori', 'drugie'];
|
|
|
|
$renderChipLinks = function (array $keys) use ($translations) {
|
|
foreach ($keys as $k) {
|
|
$label = $translations[$k] ?? $k;
|
|
$slug = seo_spec_slug_from_key($k);
|
|
|
|
$href = '/recherche/';
|
|
if ($slug !== null && $slug !== '') {
|
|
$href .= rawurlencode($slug) . '/';
|
|
}
|
|
|
|
echo '<a class="chip chip-link" href="' . h($href) . '">' . h($label) . '</a>';
|
|
}
|
|
};
|
|
?>
|
|
<!--<h2 class="home-h2"><?php echo h($translations['home_popular_specialists'] ?? '') ?></h2>-->
|
|
<h2 class="visually-hidden">
|
|
<?php echo h($translations['boostseo_home_h2_categories'] ?? '') ?>
|
|
</h2>
|
|
<div class="row"><?php $renderChipLinks($chipKeysRow1); ?></div>
|
|
<div class="row"><?php $renderChipLinks($chipKeysRow2); ?></div>
|
|
<div class="row"><?php $renderChipLinks($chipKeysRow3); ?></div>
|
|
|
|
<br>
|
|
<h2 class="visually-hidden">
|
|
<?php echo h($translations['boostseo_home_h2_recent'] ?? '') ?>
|
|
</h2>
|
|
<div class="hp-list">
|
|
<?php
|
|
try {
|
|
|
|
/* ======================================================
|
|
1️⃣ On récupère 50 IDs récents avec photo + abonnement actif
|
|
====================================================== */
|
|
$ids = $pdo->query("
|
|
SELECT id
|
|
FROM praticien
|
|
WHERE photo IS NOT NULL
|
|
AND photo <> ''
|
|
AND date_expiration > DATE_ADD(NOW(), INTERVAL 2 DAY)
|
|
ORDER BY id DESC
|
|
LIMIT 50
|
|
")->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
if (! $ids) {
|
|
throw new Exception('Aucun praticien');
|
|
}
|
|
|
|
/* ======================================================
|
|
2️⃣ On mélange et on prend 4
|
|
====================================================== */
|
|
shuffle($ids);
|
|
$ids = array_slice($ids, 0, 4);
|
|
|
|
/* ======================================================
|
|
3️⃣ On récupère les 4 profils
|
|
====================================================== */
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT id, nom, prenom, specialite, photo
|
|
FROM praticien
|
|
WHERE id IN ($placeholders)
|
|
");
|
|
$stmt->execute($ids);
|
|
|
|
$randomPros = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (! $randomPros) {
|
|
throw new Exception('Aucun praticien');
|
|
}
|
|
/* ======================================================
|
|
3️⃣ Notes
|
|
====================================================== */
|
|
function percent_from_5($note)
|
|
{
|
|
$n = max(0, min(5, (float) $note));
|
|
return (int) round($n / 5 * 100);
|
|
}
|
|
|
|
$ratingsById = [];
|
|
|
|
$stRatings = $pdo->prepare("
|
|
SELECT praticien_id, ROUND(AVG(note),1) AS m, COUNT(*) AS c
|
|
FROM avis_praticien
|
|
WHERE statut='publie' AND praticien_id IN ($placeholders)
|
|
GROUP BY praticien_id
|
|
");
|
|
$stRatings->execute($ids);
|
|
|
|
foreach ($stRatings->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
|
$ratingsById[(int) $r['praticien_id']] = [
|
|
'm' => (float) $r['m'],
|
|
'c' => (int) $r['c'],
|
|
];
|
|
}
|
|
|
|
/* ======================================================
|
|
4️⃣ Affichage
|
|
====================================================== */
|
|
foreach ($randomPros as $rp):
|
|
|
|
$pid = (int) $rp['id'];
|
|
|
|
$photoName = ltrim((string) $rp['photo'], '/');
|
|
$rpImg = "/public/uploads/" . $photoName;
|
|
$abs = APP_HTDOCS . "/public/uploads/" . $photoName;
|
|
|
|
if (! $photoName || ! is_file($abs)) {
|
|
$rpImg = "/public/uploads/img/user_placeholder.png";
|
|
}
|
|
|
|
$fullName = trim(($rp['prenom'] ?? '') . ' ' . ($rp['nom'] ?? ''));
|
|
$spec = trim((string) ($rp['specialite'] ?? ''));
|
|
$detailUrl = "/detail_public.php?id=" . $pid;
|
|
|
|
$hour = random_int(9, 18);
|
|
$availability = ($translations['home_online_at'] ?? 'Онлайн:') . ' ' . sprintf('%02d:00', $hour);
|
|
|
|
$stats = $ratingsById[$pid] ?? ['m' => 0, 'c' => 0];
|
|
$note = (float) $stats['m'];
|
|
$count = (int) $stats['c'];
|
|
$w = percent_from_5($note);
|
|
?>
|
|
<article class="hp-card">
|
|
|
|
<div class="hp-row1">
|
|
<a href="<?php echo h($detailUrl) ?>" class="hp-avatarLink">
|
|
<img class="hp-avatar" src="<?php echo h($rpImg) ?>" alt="<?php echo h($fullName) ?>" width="56" height="56" loading="lazy">
|
|
</a>
|
|
|
|
<div class="hp-main">
|
|
<a href="<?php echo h($detailUrl) ?>" class="hp-avatarLink">
|
|
<p class="hp-name"><?php echo h($fullName) ?></p>
|
|
</a>
|
|
<div class="hp-spec">
|
|
<?php
|
|
$text = $spec ?: ($translations['home_specialist_fallback'] ?? '');
|
|
$words = preg_split('/\s+/', trim($text));
|
|
echo h(implode(' ', array_slice($words, 0, 7)));
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hp-rating">
|
|
<?php if ($count > 0): ?>
|
|
<div class="hp-stars">
|
|
<div class="stars-base">★★★★★</div>
|
|
<div class="stars-fill" style="width:<?php echo (int) $w ?>%">★★★★★</div>
|
|
</div>
|
|
|
|
<div class="hp-note">
|
|
<?php echo number_format($note, 1, ',', ' ') ?>/5
|
|
<span class="text-muted">(<?php echo (int) $count ?>)</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hp-row2">
|
|
<div class="hp-online">
|
|
<span>📅</span>
|
|
<span><?php echo h($availability) ?></span>
|
|
</div>
|
|
|
|
<a class="hp-btn" href="<?php echo h($detailUrl) ?>">
|
|
<?php echo h($translations['home_btn_book'] ?? '') ?>
|
|
</a>
|
|
</div>
|
|
|
|
</article>
|
|
<?php
|
|
endforeach;
|
|
|
|
} catch (Throwable $e) {
|
|
// silencieux volontaire
|
|
}
|
|
?>
|
|
</div>
|
|
<br><br>
|
|
<form class="searchbar" action="/recherche" method="get" id="searchForm">
|
|
<div class="search-icon">🔍</div>
|
|
|
|
<div class="input-wrap">
|
|
<input type="text"
|
|
id="specialite"
|
|
name="specialite"
|
|
autocomplete="off"
|
|
placeholder="<?php echo h($translations['n_special'] ?? '') ?>"
|
|
aria-label="Специальность">
|
|
<ul class="suggest" id="sg-specialite" hidden></ul>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<div class="input-wrap">
|
|
<input type="text"
|
|
id="ville"
|
|
name="ville"
|
|
autocomplete="off"
|
|
placeholder="<?php echo h($translations['gde'] ?? '') ?>"
|
|
aria-label="Город">
|
|
<ul class="suggest" id="sg-ville" hidden></ul>
|
|
</div>
|
|
|
|
<button class="hp-btn btn-find--inbar" type="submit">
|
|
<?php echo h($translations['naiti'] ?? '') ?>
|
|
</button>
|
|
</form><br>
|
|
<div class="spec-cta-wrap">
|
|
<h3><?php echo h($translations['choix_pro_titre_home'] ?? '') ?></h3>
|
|
<p><!--<?php echo h($translations['choix_pro_texte_court'] ?? '') ?>--></p>
|
|
<a class="spec-cta" href="/inscription_praticien.php">
|
|
<span class="spec-cta__label"><?php echo h($translations['titre_spec'] ?? '') ?></span>
|
|
</a>
|
|
</div>
|
|
<br>
|
|
<!--<div class="hp-all">
|
|
<a href="/result_search.php">
|
|
<?php echo h($translations['smotret_vsex'] ?? '') ?>
|
|
</a>
|
|
</div>-->
|
|
<br>
|
|
|
|
<!-- Articles -->
|
|
<!--<h2 class="home-h2"><?php echo h($translations['home_useful_articles'] ?? '') ?></h2>-->
|
|
|
|
<?php
|
|
function fetch_url(string $url, int $timeout = 4): array
|
|
{
|
|
// Retourne [ok(bool), body(string), err(string), http(int)]
|
|
if (! function_exists('curl_init')) {
|
|
return [false, '', 'cURL non disponible', 0];
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_CONNECTTIMEOUT => $timeout,
|
|
CURLOPT_TIMEOUT => $timeout,
|
|
CURLOPT_USERAGENT => 'uslugi.fr (+https://staging.uslugi.fr)',
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_SSL_VERIFYHOST => 2,
|
|
]);
|
|
|
|
$body = (string) curl_exec($ch);
|
|
$err = curl_error($ch);
|
|
$http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
curl_close($ch);
|
|
|
|
$ok = ($err === '' && $http >= 200 && $http < 300 && $body !== '');
|
|
return [$ok, $body, $err, $http];
|
|
}
|
|
function decode_entities(string $s): string
|
|
{
|
|
// ENT_HTML5 gère correctement les entités modernes
|
|
return html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
}
|
|
|
|
function fetch_sovetnik_articles(int $limit = 4, bool $debug = false): array
|
|
{
|
|
$cacheFile = APP_HTDOCS . '/cache/sovetnik_feed.json';
|
|
$lockFile = APP_HTDOCS . '/cache/sovetnik_feed.lock';
|
|
|
|
// Soft TTL: on essaye de rafraîchir après 15 min
|
|
// Hard TTL: on accepte d'afficher un cache jusqu'à 24h (pour ne jamais bloquer la page)
|
|
$ttlSoft = 15 * 60;
|
|
$ttlHard = 24 * 60 * 60;
|
|
|
|
$now = time();
|
|
$hasCache = is_file($cacheFile);
|
|
$age = $hasCache ? ($now - filemtime($cacheFile)) : PHP_INT_MAX;
|
|
|
|
// Helper pour lire cache
|
|
$readCache = function () use ($cacheFile, $limit): array {
|
|
$data = json_decode((string) @file_get_contents($cacheFile), true);
|
|
return (is_array($data) && $data) ? array_slice($data, 0, $limit) : [];
|
|
};
|
|
|
|
// 1) Cache frais => retour immédiat (rapide)
|
|
if ($hasCache && $age < $ttlSoft) {
|
|
return $readCache();
|
|
}
|
|
|
|
// 2) Cache expiré mais pas trop vieux => on le garde en "stale" (fallback instantané)
|
|
$stale = [];
|
|
if ($hasCache && $age < $ttlHard) {
|
|
$stale = $readCache();
|
|
}
|
|
|
|
// 3) On tente un refresh, MAIS sans bloquer longtemps
|
|
// + lock pour éviter que plusieurs requêtes refreshent en même temps
|
|
$canRefresh = true;
|
|
|
|
if (is_file($lockFile)) {
|
|
// lock encore récent => on ne refresh pas
|
|
if (($now - filemtime($lockFile)) < 60) {
|
|
$canRefresh = false;
|
|
}
|
|
}
|
|
|
|
if (! $canRefresh) {
|
|
// On sert le stale cache (ou vide)
|
|
return $stale;
|
|
}
|
|
|
|
// On pose le lock (best-effort)
|
|
@is_dir(dirname($lockFile)) || @mkdir(dirname($lockFile), 0755, true);
|
|
@file_put_contents($lockFile, (string) $now);
|
|
|
|
// Timeout court = clé de la perf
|
|
$timeout = 1;
|
|
|
|
// 1) RSS
|
|
$feedCandidates = [
|
|
'https://www.sovetnik.fr/feed/',
|
|
'https://www.sovetnik.fr/?feed=rss2',
|
|
'https://www.sovetnik.fr/feed/rss2/',
|
|
];
|
|
|
|
foreach ($feedCandidates as $feedUrl) {
|
|
[$ok, $xmlStr, $err, $http] = fetch_url($feedUrl, $timeout);
|
|
|
|
if ($debug) {
|
|
echo "\n<!-- Sovetnik RSS try: $feedUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
|
}
|
|
|
|
if (! $ok) {
|
|
continue;
|
|
}
|
|
|
|
libxml_use_internal_errors(true);
|
|
$xml = simplexml_load_string($xmlStr);
|
|
if (! $xml || empty($xml->channel->item)) {
|
|
continue;
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($xml->channel->item as $item) {
|
|
$titleRaw = (string) $item->title;
|
|
$descRaw = (string) $item->description;
|
|
|
|
$out[] = [
|
|
'url' => (string) $item->link,
|
|
'title' => trim(decode_entities(strip_tags($titleRaw))),
|
|
'desc' => trim(decode_entities(strip_tags($descRaw))),
|
|
'img' => '',
|
|
];
|
|
|
|
if (count($out) >= $limit) {
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if ($out) {
|
|
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
|
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
|
@unlink($lockFile);
|
|
return $out;
|
|
}
|
|
}
|
|
|
|
// 2) Fallback API
|
|
$apiUrl = 'https://www.sovetnik.fr/wp-json/wp/v2/posts?per_page=' . (int) $limit . '&_embed=1';
|
|
[$ok, $jsonStr, $err, $http] = fetch_url($apiUrl, $timeout);
|
|
|
|
if ($debug) {
|
|
echo "\n<!-- Sovetnik API try: $apiUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
|
}
|
|
|
|
if ($ok) {
|
|
$arr = json_decode($jsonStr, true);
|
|
if (is_array($arr)) {
|
|
$out = [];
|
|
foreach ($arr as $p) {
|
|
$title = trim(decode_entities(strip_tags($p['title']['rendered'] ?? '')));
|
|
$desc = trim(decode_entities(strip_tags($p['excerpt']['rendered'] ?? '')));
|
|
|
|
$img = '';
|
|
if (! empty($p['_embedded']['wp:featuredmedia'][0]['source_url'])) {
|
|
$img = (string) $p['_embedded']['wp:featuredmedia'][0]['source_url'];
|
|
}
|
|
|
|
$out[] = [
|
|
'url' => (string) ($p['link'] ?? ''),
|
|
'title' => $title,
|
|
'desc' => $desc,
|
|
'img' => $img,
|
|
];
|
|
|
|
if (count($out) >= $limit) {
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if ($out) {
|
|
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
|
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
|
@unlink($lockFile);
|
|
return $out;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Échec refresh => on enlève le lock et on sert le stale
|
|
@unlink($lockFile);
|
|
return $stale;
|
|
}
|
|
|
|
// Debug optionnel
|
|
$debugArticles = isset($_GET['debug']) && $_GET['debug'] === '1';
|
|
$articles = fetch_sovetnik_articles(4, $debugArticles);
|
|
?>
|
|
|
|
|
|
|
|
<div class="hp-articles">
|
|
<?php if (! empty($articles)): ?>
|
|
<?php foreach ($articles as $a): ?>
|
|
<a class="ha" href="<?php echo h($a['url']) ?>" target="_blank" rel="noopener">
|
|
<div class="ha-img" style="<?php echo ! empty($a['img']) ? 'background:url(' . h($a['img']) . ') top center/cover no-repeat;' : '' ?>"></div>
|
|
<div class="ha-body">
|
|
<p class="ha-title"><?php echo h($a['title']) ?></p>
|
|
<p class="ha-sub"><?php echo h(mb_strimwidth($a['desc'], 0, 120, '…', 'UTF-8')) ?></p>
|
|
</div>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<div class="text-muted"><?php echo h($translations['home_articles_unavailable'] ?? 'Articles indisponibles pour le moment.') ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!--<div class="hp-all">
|
|
<a href="https://www.sovetnik.fr/" target="_blank" rel="noopener">
|
|
<?php echo h($translations['home_all_articles'] ?? '') ?>
|
|
</a>
|
|
</div>-->
|
|
|
|
<br>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
<div class="visually-hidden">
|
|
<h2><?php echo h($translations['boostseo_home_h2_cities'] ?? '') ?></h2>
|
|
<p><?php echo h($translations['boostseo_home_cities_text'] ?? '') ?></p>
|
|
</div>
|
|
<footer class="site-footer">
|
|
<div class="container">
|
|
<p><?php echo $translations['copyr'] ?? '' ?><a href="/terms_fr.php" target="_blank"></a></p>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- ✅ Scripts recherche + submit (chips = liens donc pas de JS chips) -->
|
|
<script src="/js/home.js"></script>
|
|
|
|
<!-- ✅ Bootstrap -->
|
|
<script src="/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<?php include __DIR__ . '/includes/accepter_cookies.php'; ?>
|
|
</body>
|
|
</html>
|