Files
maria_uslugi/index_sovetnik.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>