mon site ok 222
This commit is contained in:
parent
b1894c71eb
commit
cd414e4566
49
api/visit-counter.php
Normal file
49
api/visit-counter.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
|
||||
require_once __DIR__ . '/../db/visit_counter.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
try {
|
||||
if ($method === 'POST') {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$payload = json_decode(is_string($rawBody) ? $rawBody : '', true);
|
||||
if (!is_array($payload)) {
|
||||
$payload = $_POST;
|
||||
}
|
||||
|
||||
$action = is_string($payload['action'] ?? null) ? $payload['action'] : 'ping';
|
||||
$token = is_string($payload['token'] ?? null) ? $payload['token'] : null;
|
||||
|
||||
if ($action === 'forget') {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'counts' => visit_counter_forget($token),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$tracked = visit_counter_track($token);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'token' => $tracked['token'],
|
||||
'counts' => $tracked['counts'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'counts' => visit_counter_snapshot(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'visit_counter_unavailable',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@ -35,6 +35,10 @@ body {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
body.cookie-consent-locked {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
text-decoration-color: rgba(17, 24, 39, 0.35);
|
||||
@ -312,6 +316,30 @@ a:focus-visible {
|
||||
|
||||
.stat-grid span {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.stat-grid--counter div:first-child {
|
||||
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.stat-grid--counter div:first-child strong,
|
||||
.stat-grid--counter div:first-child span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-grid--counter strong {
|
||||
font-size: clamp(1.2rem, 2vw, 1.45rem);
|
||||
}
|
||||
|
||||
.panel-note {
|
||||
margin-top: 0.85rem;
|
||||
font-size: 0.84rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.visit-counter-panel .panel-copy {
|
||||
min-height: 3.5rem;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
@ -642,12 +670,22 @@ a:focus-visible {
|
||||
background: #dff0d8;
|
||||
}
|
||||
|
||||
.cookie-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1085;
|
||||
background: rgba(17, 24, 39, 0.58);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
z-index: 1045;
|
||||
z-index: 1095;
|
||||
width: min(480px, calc(100vw - 2rem));
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--line-strong);
|
||||
@ -655,6 +693,15 @@ a:focus-visible {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.cookie-banner--modal {
|
||||
top: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
width: min(520px, calc(100vw - 2rem));
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.cookie-banner[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
@ -819,6 +866,16 @@ a:focus-visible {
|
||||
bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cookie-banner.cookie-banner--modal {
|
||||
top: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
width: calc(100vw - 1rem);
|
||||
max-height: calc(100vh - 1rem);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.cookie-floating {
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const COOKIE_NAME = 'ptcs_consent';
|
||||
const VISIT_COOKIE_NAME = 'ptcs_visit_id';
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 180;
|
||||
const VISIT_REFRESH_MS = 30 * 1000;
|
||||
const STORAGE_KEYS = {
|
||||
personalization: 'ptcs_ui_preferences',
|
||||
audience: 'ptcs_local_audience'
|
||||
personalization: 'ptcs_ui_preferences'
|
||||
};
|
||||
|
||||
const banner = document.getElementById('cookie-banner');
|
||||
const overlay = document.getElementById('cookie-overlay');
|
||||
const reopenButton = document.getElementById('cookie-reopen');
|
||||
const floatingReminder = reopenButton ? reopenButton.closest('.cookie-floating') : null;
|
||||
const reopenLabel = document.getElementById('cookie-reopen-label');
|
||||
const footerCookieSettings = document.getElementById('footer-cookie-settings');
|
||||
const dismissScrollHint = document.getElementById('scroll-hint-dismiss');
|
||||
@ -16,7 +19,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const personalizationControls = Array.from(document.querySelectorAll('[data-consent-control="personalization"]'));
|
||||
const audienceControls = Array.from(document.querySelectorAll('[data-consent-control="audience"]'));
|
||||
const consentActionButtons = Array.from(document.querySelectorAll('[data-cookie-action]'));
|
||||
const trackedSections = Array.from(document.querySelectorAll('[data-track-section]'));
|
||||
const isHomePage = document.body?.dataset.page === 'home';
|
||||
const bodyStartsLocked = document.body?.dataset.cookieLock === 'pending';
|
||||
|
||||
const visitCounterRoot = document.querySelector('[data-visit-counter-endpoint]');
|
||||
const visitCounterEndpoint = visitCounterRoot?.dataset.visitCounterEndpoint || '/api/visit-counter.php';
|
||||
const liveWindowMinutes = Number(visitCounterRoot?.dataset.liveWindowMinutes || 5);
|
||||
const visitStatusNodes = Array.from(document.querySelectorAll('[data-visit-status]'));
|
||||
const visitLiveNodes = Array.from(document.querySelectorAll('[data-visit-live]'));
|
||||
const visitDailyNodes = Array.from(document.querySelectorAll('[data-visit-daily]'));
|
||||
const visitTotalNodes = Array.from(document.querySelectorAll('[data-visit-total]'));
|
||||
const visitUpdatedNodes = Array.from(document.querySelectorAll('[data-visit-updated]'));
|
||||
const numberFormatter = new Intl.NumberFormat('fr-FR');
|
||||
|
||||
const state = {
|
||||
prefs: {
|
||||
@ -25,8 +39,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
audience: false,
|
||||
timestamp: null
|
||||
},
|
||||
audienceCountedThisSession: false,
|
||||
visibleSections: new Set()
|
||||
hasSavedDecision: false,
|
||||
requiresBlockingChoice: bodyStartsLocked,
|
||||
visitCounts: visitCounterRoot ? {
|
||||
live: Number(visitCounterRoot.dataset.initialLive || 0),
|
||||
daily: Number(visitCounterRoot.dataset.initialDaily || 0),
|
||||
total: Number(visitCounterRoot.dataset.initialTotal || 0),
|
||||
updated_at: visitCounterRoot.dataset.updatedAt || null,
|
||||
updated_label: visitCounterRoot.dataset.updatedLabel || ''
|
||||
} : null,
|
||||
visitRefreshTimer: null
|
||||
};
|
||||
|
||||
function safeParse(rawValue) {
|
||||
@ -79,6 +101,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
||||
}
|
||||
|
||||
function deleteCookie(name) {
|
||||
document.cookie = `${name}=; path=/; max-age=0; SameSite=Lax`;
|
||||
}
|
||||
|
||||
function normalizePrefs(rawPrefs = {}) {
|
||||
return {
|
||||
essential: true,
|
||||
@ -106,7 +132,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
function formatNumber(value) {
|
||||
const numericValue = Number(value);
|
||||
return numberFormatter.format(Number.isFinite(numericValue) ? numericValue : 0);
|
||||
}
|
||||
|
||||
function formatTime(dateString) {
|
||||
if (!dateString) {
|
||||
return '—';
|
||||
}
|
||||
@ -115,15 +146,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return '—';
|
||||
}
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function updateSummaryText() {
|
||||
const summary = state.prefs.personalization || state.prefs.audience
|
||||
? `Réglages actifs : essentiels, ${state.prefs.personalization ? 'personnalisation' : 'personnalisation coupée'} et ${state.prefs.audience ? 'mesure locale activée' : 'mesure locale désactivée'}.`
|
||||
: 'Le site conserve uniquement l\'essentiel tant que vous n\'avez pas choisi d\'options supplémentaires.';
|
||||
let summary = "Le site conserve uniquement l'essentiel tant que vous n'avez pas choisi d'options supplémentaires.";
|
||||
|
||||
if (state.prefs.personalization && state.prefs.audience) {
|
||||
summary = 'Réglages actifs : essentiels, personnalisation et compteur visiteurs anonymisé.';
|
||||
} else if (state.prefs.personalization) {
|
||||
summary = 'Réglages actifs : essentiels et personnalisation. Le compteur visiteurs reste inactif.';
|
||||
} else if (state.prefs.audience) {
|
||||
summary = "Réglages actifs : essentiels et compteur visiteurs anonymisé. Les préférences d'interface restent minimales.";
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-consent-summary]').forEach((node) => {
|
||||
node.textContent = summary;
|
||||
@ -148,27 +186,68 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
reopenLabel.textContent = 'Cookies : essentiels';
|
||||
}
|
||||
|
||||
function updateAudiencePanel(store) {
|
||||
const audienceEnabled = state.prefs.audience;
|
||||
const visits = audienceEnabled && store ? Number(store.visits || 0) : 0;
|
||||
const sections = audienceEnabled && store && Array.isArray(store.sections) ? store.sections.length : 0;
|
||||
const lastVisit = audienceEnabled && store ? formatDate(store.lastVisit || null) : '—';
|
||||
const statusText = audienceEnabled
|
||||
? 'La mesure locale d\'audience est activée sur cet appareil. Les compteurs ci-dessous sont stockés uniquement dans votre navigateur.'
|
||||
: 'La mesure locale d\'audience est désactivée. Aucun service tiers n\'est utilisé.';
|
||||
function focusBannerPrimaryAction() {
|
||||
if (!banner) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-audience-status]').forEach((node) => {
|
||||
node.textContent = statusText;
|
||||
});
|
||||
document.querySelectorAll('[data-audience-visits]').forEach((node) => {
|
||||
node.textContent = String(visits);
|
||||
});
|
||||
document.querySelectorAll('[data-audience-sections]').forEach((node) => {
|
||||
node.textContent = String(sections);
|
||||
});
|
||||
document.querySelectorAll('[data-audience-last-visit]').forEach((node) => {
|
||||
node.textContent = lastVisit;
|
||||
});
|
||||
const primaryAction = banner.querySelector('[data-cookie-action="reject"]')
|
||||
|| banner.querySelector('[data-cookie-action="save"]')
|
||||
|| banner.querySelector('button, [href], input:not([disabled])');
|
||||
|
||||
if (primaryAction instanceof HTMLElement) {
|
||||
primaryAction.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (banner instanceof HTMLElement) {
|
||||
banner.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
function syncConsentBarrier() {
|
||||
const shouldLock = isHomePage && state.requiresBlockingChoice && !state.hasSavedDecision;
|
||||
|
||||
document.body.classList.toggle('cookie-consent-locked', shouldLock);
|
||||
document.body.dataset.cookieLock = shouldLock ? 'pending' : 'released';
|
||||
|
||||
if (overlay) {
|
||||
overlay.hidden = !shouldLock;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
banner.classList.toggle('cookie-banner--modal', shouldLock);
|
||||
banner.setAttribute('aria-modal', shouldLock ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const shouldHideReminder = !state.hasSavedDecision && banner && !banner.hidden;
|
||||
|
||||
if (floatingReminder) {
|
||||
floatingReminder.hidden = shouldHideReminder;
|
||||
}
|
||||
|
||||
if (reopenButton) {
|
||||
reopenButton.hidden = false;
|
||||
if (shouldHideReminder) {
|
||||
reopenButton.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
reopenButton.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPersonalizationUI() {
|
||||
const stored = safeParse(storageGet(STORAGE_KEYS.personalization)) || {};
|
||||
if (!scrollHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.prefs.personalization) {
|
||||
scrollHint.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
scrollHint.hidden = stored.scrollHintDismissed === true;
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
@ -194,65 +273,196 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
function readAudienceStore() {
|
||||
const parsed = safeParse(storageGet(STORAGE_KEYS.audience));
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return {
|
||||
visits: 0,
|
||||
sections: [],
|
||||
lastVisit: null
|
||||
function updateVisitCounter(snapshot = null) {
|
||||
if (!visitCounterRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot && typeof snapshot === 'object') {
|
||||
state.visitCounts = {
|
||||
live: Number(snapshot.live || 0),
|
||||
daily: Number(snapshot.daily || 0),
|
||||
total: Number(snapshot.total || 0),
|
||||
updated_at: snapshot.updated_at || null,
|
||||
updated_label: snapshot.updated_label || ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
visits: Number(parsed.visits || 0),
|
||||
sections: Array.isArray(parsed.sections) ? parsed.sections : [],
|
||||
lastVisit: parsed.lastVisit || null
|
||||
|
||||
const counts = state.visitCounts || {
|
||||
live: 0,
|
||||
daily: 0,
|
||||
total: 0,
|
||||
updated_at: null,
|
||||
updated_label: ''
|
||||
};
|
||||
|
||||
visitLiveNodes.forEach((node) => {
|
||||
node.textContent = formatNumber(counts.live);
|
||||
});
|
||||
visitDailyNodes.forEach((node) => {
|
||||
node.textContent = formatNumber(counts.daily);
|
||||
});
|
||||
visitTotalNodes.forEach((node) => {
|
||||
node.textContent = formatNumber(counts.total);
|
||||
});
|
||||
|
||||
let statusText = `Le compteur affiche l'audience mesurée sur le site. “En direct” correspond aux visiteurs actifs sur les ${liveWindowMinutes} dernières minutes.`;
|
||||
if (!state.hasSavedDecision) {
|
||||
statusText = "Le compteur est visible, mais votre visite ne sera comptée qu’après votre choix cookies.";
|
||||
} else if (state.prefs.audience) {
|
||||
statusText = "Le compteur anonyme est actif pour votre visite. Les chiffres se mettent à jour automatiquement sans service tiers.";
|
||||
} else {
|
||||
statusText = "Le compteur reste en lecture seule tant que la mesure locale d’audience est désactivée. Votre visite actuelle n’est pas ajoutée.";
|
||||
}
|
||||
|
||||
visitStatusNodes.forEach((node) => {
|
||||
node.textContent = statusText;
|
||||
});
|
||||
|
||||
const timeLabel = counts.updated_label || formatTime(counts.updated_at);
|
||||
const updatedText = `Dernière mise à jour : ${timeLabel} · “En direct” = visiteurs actifs sur les ${liveWindowMinutes} dernières minutes.`;
|
||||
visitUpdatedNodes.forEach((node) => {
|
||||
node.textContent = updatedText;
|
||||
});
|
||||
}
|
||||
|
||||
function saveAudienceStore(store) {
|
||||
storageSet(STORAGE_KEYS.audience, JSON.stringify(store));
|
||||
async function requestVisitCounter(method = 'GET', payload = null) {
|
||||
if (!visitCounterRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store'
|
||||
};
|
||||
|
||||
if (payload !== null) {
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
options.body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(visitCounterEndpoint, options);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data && data.success ? data : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPersonalizationUI() {
|
||||
const stored = safeParse(storageGet(STORAGE_KEYS.personalization)) || {};
|
||||
if (!scrollHint) {
|
||||
function clearVisitRefreshTimer() {
|
||||
if (state.visitRefreshTimer) {
|
||||
window.clearInterval(state.visitRefreshTimer);
|
||||
state.visitRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startVisitRefreshTimer() {
|
||||
if (!visitCounterRoot || state.visitRefreshTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.prefs.personalization) {
|
||||
scrollHint.hidden = false;
|
||||
return;
|
||||
}
|
||||
state.visitRefreshTimer = window.setInterval(() => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollHint.hidden = stored.scrollHintDismissed === true;
|
||||
if (state.prefs.audience) {
|
||||
void pingVisitCounter();
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshVisitSnapshot();
|
||||
}, VISIT_REFRESH_MS);
|
||||
}
|
||||
|
||||
function applyAudienceState() {
|
||||
if (!state.prefs.audience) {
|
||||
storageRemove(STORAGE_KEYS.audience);
|
||||
state.audienceCountedThisSession = false;
|
||||
updateAudiencePanel(null);
|
||||
async function refreshVisitSnapshot() {
|
||||
const data = await requestVisitCounter('GET');
|
||||
if (data && data.counts) {
|
||||
updateVisitCounter(data.counts);
|
||||
return;
|
||||
}
|
||||
updateVisitCounter();
|
||||
}
|
||||
|
||||
async function pingVisitCounter() {
|
||||
const token = getCookie(VISIT_COOKIE_NAME);
|
||||
const data = await requestVisitCounter('POST', {
|
||||
action: 'ping',
|
||||
token
|
||||
});
|
||||
|
||||
if (data?.token) {
|
||||
setCookie(VISIT_COOKIE_NAME, data.token, COOKIE_MAX_AGE);
|
||||
}
|
||||
|
||||
if (data?.counts) {
|
||||
updateVisitCounter(data.counts);
|
||||
return;
|
||||
}
|
||||
|
||||
const store = readAudienceStore();
|
||||
if (!state.audienceCountedThisSession) {
|
||||
store.visits += 1;
|
||||
store.lastVisit = new Date().toISOString();
|
||||
state.audienceCountedThisSession = true;
|
||||
updateVisitCounter();
|
||||
}
|
||||
|
||||
async function forgetVisitCounter() {
|
||||
const token = getCookie(VISIT_COOKIE_NAME);
|
||||
deleteCookie(VISIT_COOKIE_NAME);
|
||||
|
||||
if (!token) {
|
||||
await refreshVisitSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.visibleSections.size > 0) {
|
||||
const merged = new Set([...(store.sections || []), ...state.visibleSections]);
|
||||
store.sections = Array.from(merged);
|
||||
const data = await requestVisitCounter('POST', {
|
||||
action: 'forget',
|
||||
token
|
||||
});
|
||||
|
||||
if (data?.counts) {
|
||||
updateVisitCounter(data.counts);
|
||||
return;
|
||||
}
|
||||
|
||||
saveAudienceStore(store);
|
||||
updateAudiencePanel(store);
|
||||
await refreshVisitSnapshot();
|
||||
}
|
||||
|
||||
function syncVisitCounter(previousAudienceEnabled = false) {
|
||||
if (!visitCounterRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearVisitRefreshTimer();
|
||||
|
||||
if (state.prefs.audience) {
|
||||
void pingVisitCounter().finally(() => {
|
||||
startVisitRefreshTimer();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousAudienceEnabled) {
|
||||
void forgetVisitCounter().finally(() => {
|
||||
startVisitRefreshTimer();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
deleteCookie(VISIT_COOKIE_NAME);
|
||||
void refreshVisitSnapshot().finally(() => {
|
||||
startVisitRefreshTimer();
|
||||
});
|
||||
}
|
||||
|
||||
function applyPrefs(prefs) {
|
||||
const previousAudienceEnabled = Boolean(state.prefs.audience);
|
||||
state.prefs = normalizePrefs(prefs);
|
||||
document.body.dataset.consentPersonalization = state.prefs.personalization ? 'on' : 'off';
|
||||
document.body.dataset.consentAudience = state.prefs.audience ? 'on' : 'off';
|
||||
@ -269,9 +479,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
refreshPersonalizationUI();
|
||||
applyAudienceState();
|
||||
updateSummaryText();
|
||||
updateReminderLabel();
|
||||
updateVisitCounter();
|
||||
syncVisitCounter(previousAudienceEnabled);
|
||||
}
|
||||
|
||||
function openBanner() {
|
||||
@ -279,9 +490,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
banner.hidden = false;
|
||||
syncConsentBarrier();
|
||||
if (reopenButton) {
|
||||
reopenButton.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
focusBannerPrimaryAction();
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
@ -289,6 +502,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
banner.hidden = true;
|
||||
syncConsentBarrier();
|
||||
if (reopenButton) {
|
||||
reopenButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
@ -298,6 +512,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const normalized = normalizePrefs(nextPrefs);
|
||||
normalized.timestamp = new Date().toISOString();
|
||||
setCookie(COOKIE_NAME, JSON.stringify(normalized), COOKIE_MAX_AGE);
|
||||
state.hasSavedDecision = true;
|
||||
state.requiresBlockingChoice = false;
|
||||
applyPrefs(normalized);
|
||||
closeBanner();
|
||||
showToast(notice);
|
||||
@ -357,36 +573,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
if ('IntersectionObserver' in window && trackedSections.length > 0) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
let changed = false;
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!visitCounterRoot || document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = entry.target.id || entry.target.dataset.trackSection || '';
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
if (state.prefs.audience) {
|
||||
void pingVisitCounter();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.visibleSections.has(id)) {
|
||||
state.visibleSections.add(id);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (changed && state.prefs.audience) {
|
||||
applyAudienceState();
|
||||
}
|
||||
}, {
|
||||
threshold: 0.45
|
||||
});
|
||||
|
||||
trackedSections.forEach((section) => observer.observe(section));
|
||||
}
|
||||
void refreshVisitSnapshot();
|
||||
});
|
||||
|
||||
const savedPrefs = readSavedPrefs();
|
||||
state.hasSavedDecision = Boolean(savedPrefs);
|
||||
state.requiresBlockingChoice = isHomePage && !savedPrefs;
|
||||
|
||||
if (savedPrefs) {
|
||||
applyPrefs(savedPrefs);
|
||||
closeBanner();
|
||||
|
||||
27
db/migrations/20260502_create_visit_counter_tables.sql
Normal file
27
db/migrations/20260502_create_visit_counter_tables.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- 2026-05-02: compteur visiteurs live / journalier / total
|
||||
-- Tables first-party pour mesurer anonymement les visites après consentement audience.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS visit_counter_sessions (
|
||||
visit_token CHAR(48) NOT NULL,
|
||||
first_seen_at DATETIME NOT NULL,
|
||||
last_seen_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (visit_token),
|
||||
KEY idx_last_seen_at (last_seen_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS visit_counter_daily (
|
||||
visit_token CHAR(48) NOT NULL,
|
||||
visit_date DATE NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (visit_token, visit_date),
|
||||
KEY idx_visit_date (visit_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS visit_counter_meta (
|
||||
meta_key VARCHAR(64) NOT NULL,
|
||||
meta_value BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (meta_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
278
db/visit_counter.php
Normal file
278
db/visit_counter.php
Normal file
@ -0,0 +1,278 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
const VISIT_COUNTER_LIVE_WINDOW_MINUTES = 5;
|
||||
const VISIT_COUNTER_RETENTION_MONTHS = 13;
|
||||
const VISIT_COUNTER_TOTAL_KEY = 'lifetime_total';
|
||||
|
||||
function visit_counter_timezone(): DateTimeZone
|
||||
{
|
||||
static $timezone = null;
|
||||
if ($timezone instanceof DateTimeZone) {
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
$timezone = new DateTimeZone('Europe/Paris');
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
function visit_counter_now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('now', visit_counter_timezone());
|
||||
}
|
||||
|
||||
function visit_counter_normalize_token(mixed $token): ?string
|
||||
{
|
||||
if (!is_string($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim($token));
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_match('/\A[a-f0-9]{48}\z/', $normalized) === 1 ? $normalized : null;
|
||||
}
|
||||
|
||||
function visit_counter_generate_token(): string
|
||||
{
|
||||
return bin2hex(random_bytes(24));
|
||||
}
|
||||
|
||||
function visit_counter_ensure_schema(): void
|
||||
{
|
||||
static $initialized = false;
|
||||
if ($initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS visit_counter_sessions ('
|
||||
. ' visit_token CHAR(48) NOT NULL,'
|
||||
. ' first_seen_at DATETIME NOT NULL,'
|
||||
. ' last_seen_at DATETIME NOT NULL,'
|
||||
. ' created_at DATETIME NOT NULL,'
|
||||
. ' updated_at DATETIME NOT NULL,'
|
||||
. ' PRIMARY KEY (visit_token),'
|
||||
. ' KEY idx_last_seen_at (last_seen_at)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS visit_counter_daily ('
|
||||
. ' visit_token CHAR(48) NOT NULL,'
|
||||
. ' visit_date DATE NOT NULL,'
|
||||
. ' created_at DATETIME NOT NULL,'
|
||||
. ' PRIMARY KEY (visit_token, visit_date),'
|
||||
. ' KEY idx_visit_date (visit_date)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS visit_counter_meta ('
|
||||
. ' meta_key VARCHAR(64) NOT NULL,'
|
||||
. ' meta_value BIGINT UNSIGNED NOT NULL DEFAULT 0,'
|
||||
. ' updated_at DATETIME NOT NULL,'
|
||||
. ' PRIMARY KEY (meta_key)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||
);
|
||||
|
||||
$initialized = true;
|
||||
}
|
||||
|
||||
function visit_counter_prune_stale_data(?DateTimeImmutable $now = null): void
|
||||
{
|
||||
static $pruned = false;
|
||||
if ($pruned) {
|
||||
return;
|
||||
}
|
||||
|
||||
visit_counter_ensure_schema();
|
||||
$pdo = db();
|
||||
$now = $now ?? visit_counter_now();
|
||||
|
||||
$sessionCutoff = $now->modify('-' . VISIT_COUNTER_RETENTION_MONTHS . ' months')->format('Y-m-d H:i:s');
|
||||
$dailyCutoff = $now->modify('-' . VISIT_COUNTER_RETENTION_MONTHS . ' months')->format('Y-m-d');
|
||||
|
||||
$deleteDaily = $pdo->prepare('DELETE FROM visit_counter_daily WHERE visit_date < :cutoff');
|
||||
$deleteDaily->bindValue(':cutoff', $dailyCutoff);
|
||||
$deleteDaily->execute();
|
||||
|
||||
$deleteSessions = $pdo->prepare('DELETE FROM visit_counter_sessions WHERE last_seen_at < :cutoff');
|
||||
$deleteSessions->bindValue(':cutoff', $sessionCutoff);
|
||||
$deleteSessions->execute();
|
||||
|
||||
$pruned = true;
|
||||
}
|
||||
|
||||
function visit_counter_snapshot(?DateTimeImmutable $now = null): array
|
||||
{
|
||||
visit_counter_ensure_schema();
|
||||
visit_counter_prune_stale_data($now);
|
||||
|
||||
$pdo = db();
|
||||
$now = $now ?? visit_counter_now();
|
||||
$threshold = $now->modify('-' . VISIT_COUNTER_LIVE_WINDOW_MINUTES . ' minutes')->format('Y-m-d H:i:s');
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
$liveStmt = $pdo->prepare('SELECT COUNT(*) FROM visit_counter_sessions WHERE last_seen_at >= :threshold');
|
||||
$liveStmt->bindValue(':threshold', $threshold);
|
||||
$liveStmt->execute();
|
||||
$live = (int) $liveStmt->fetchColumn();
|
||||
|
||||
$dailyStmt = $pdo->prepare('SELECT COUNT(*) FROM visit_counter_daily WHERE visit_date = :visit_date');
|
||||
$dailyStmt->bindValue(':visit_date', $today);
|
||||
$dailyStmt->execute();
|
||||
$daily = (int) $dailyStmt->fetchColumn();
|
||||
|
||||
$totalStmt = $pdo->prepare('SELECT meta_value FROM visit_counter_meta WHERE meta_key = :meta_key');
|
||||
$totalStmt->bindValue(':meta_key', VISIT_COUNTER_TOTAL_KEY);
|
||||
$totalStmt->execute();
|
||||
$totalValue = $totalStmt->fetchColumn();
|
||||
$total = $totalValue === false ? 0 : (int) $totalValue;
|
||||
|
||||
if ($total === 0) {
|
||||
$fallbackStmt = $pdo->query('SELECT COUNT(*) FROM visit_counter_sessions');
|
||||
$total = (int) $fallbackStmt->fetchColumn();
|
||||
}
|
||||
|
||||
return [
|
||||
'live' => $live,
|
||||
'daily' => $daily,
|
||||
'total' => $total,
|
||||
'live_window_minutes' => VISIT_COUNTER_LIVE_WINDOW_MINUTES,
|
||||
'updated_at' => $now->format(DATE_ATOM),
|
||||
'updated_label' => $now->format('H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
function visit_counter_increment_total(string $timestamp): void
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO visit_counter_meta (meta_key, meta_value, updated_at) VALUES (:meta_key, :meta_value, :updated_at) '
|
||||
. 'ON DUPLICATE KEY UPDATE meta_value = meta_value + 1, updated_at = :updated_at_refresh'
|
||||
);
|
||||
$stmt->bindValue(':meta_key', VISIT_COUNTER_TOTAL_KEY);
|
||||
$stmt->bindValue(':meta_value', 1, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':updated_at', $timestamp);
|
||||
$stmt->bindValue(':updated_at_refresh', $timestamp);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function visit_counter_decrement_total(string $timestamp): void
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO visit_counter_meta (meta_key, meta_value, updated_at) VALUES (:meta_key, :meta_value, :updated_at) '
|
||||
. 'ON DUPLICATE KEY UPDATE meta_value = CASE WHEN meta_value > 0 THEN meta_value - 1 ELSE 0 END, updated_at = :updated_at_refresh'
|
||||
);
|
||||
$stmt->bindValue(':meta_key', VISIT_COUNTER_TOTAL_KEY);
|
||||
$stmt->bindValue(':meta_value', 0, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':updated_at', $timestamp);
|
||||
$stmt->bindValue(':updated_at_refresh', $timestamp);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function visit_counter_track(?string $token = null): array
|
||||
{
|
||||
visit_counter_ensure_schema();
|
||||
$pdo = db();
|
||||
$now = visit_counter_now();
|
||||
visit_counter_prune_stale_data($now);
|
||||
|
||||
$visitToken = visit_counter_normalize_token($token) ?? visit_counter_generate_token();
|
||||
$timestamp = $now->format('Y-m-d H:i:s');
|
||||
$visitDate = $now->format('Y-m-d');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$insertSession = $pdo->prepare(
|
||||
'INSERT IGNORE INTO visit_counter_sessions (visit_token, first_seen_at, last_seen_at, created_at, updated_at) '
|
||||
. 'VALUES (:visit_token, :first_seen_at, :last_seen_at, :created_at, :updated_at)'
|
||||
);
|
||||
$insertSession->bindValue(':visit_token', $visitToken);
|
||||
$insertSession->bindValue(':first_seen_at', $timestamp);
|
||||
$insertSession->bindValue(':last_seen_at', $timestamp);
|
||||
$insertSession->bindValue(':created_at', $timestamp);
|
||||
$insertSession->bindValue(':updated_at', $timestamp);
|
||||
$insertSession->execute();
|
||||
|
||||
$isNewVisitor = $insertSession->rowCount() === 1;
|
||||
|
||||
if (!$isNewVisitor) {
|
||||
$updateSession = $pdo->prepare(
|
||||
'UPDATE visit_counter_sessions SET last_seen_at = :last_seen_at, updated_at = :updated_at WHERE visit_token = :visit_token'
|
||||
);
|
||||
$updateSession->bindValue(':last_seen_at', $timestamp);
|
||||
$updateSession->bindValue(':updated_at', $timestamp);
|
||||
$updateSession->bindValue(':visit_token', $visitToken);
|
||||
$updateSession->execute();
|
||||
}
|
||||
|
||||
$insertDaily = $pdo->prepare(
|
||||
'INSERT IGNORE INTO visit_counter_daily (visit_token, visit_date, created_at) VALUES (:visit_token, :visit_date, :created_at)'
|
||||
);
|
||||
$insertDaily->bindValue(':visit_token', $visitToken);
|
||||
$insertDaily->bindValue(':visit_date', $visitDate);
|
||||
$insertDaily->bindValue(':created_at', $timestamp);
|
||||
$insertDaily->execute();
|
||||
|
||||
if ($isNewVisitor) {
|
||||
visit_counter_increment_total($timestamp);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [
|
||||
'token' => $visitToken,
|
||||
'counts' => visit_counter_snapshot($now),
|
||||
];
|
||||
}
|
||||
|
||||
function visit_counter_forget(?string $token = null): array
|
||||
{
|
||||
visit_counter_ensure_schema();
|
||||
$pdo = db();
|
||||
$now = visit_counter_now();
|
||||
visit_counter_prune_stale_data($now);
|
||||
|
||||
$visitToken = visit_counter_normalize_token($token);
|
||||
if ($visitToken === null) {
|
||||
return visit_counter_snapshot($now);
|
||||
}
|
||||
|
||||
$timestamp = $now->format('Y-m-d H:i:s');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$deleteDaily = $pdo->prepare('DELETE FROM visit_counter_daily WHERE visit_token = :visit_token');
|
||||
$deleteDaily->bindValue(':visit_token', $visitToken);
|
||||
$deleteDaily->execute();
|
||||
|
||||
$deleteSession = $pdo->prepare('DELETE FROM visit_counter_sessions WHERE visit_token = :visit_token');
|
||||
$deleteSession->bindValue(':visit_token', $visitToken);
|
||||
$deleteSession->execute();
|
||||
|
||||
if ($deleteSession->rowCount() > 0) {
|
||||
visit_counter_decrement_total($timestamp);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return visit_counter_snapshot($now);
|
||||
}
|
||||
57
index.php
57
index.php
@ -3,10 +3,25 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/site.php';
|
||||
|
||||
$site = site_settings();
|
||||
$pageTitle = 'Programme TV ce soir et en ce moment | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
|
||||
$fallbackDescription = 'Programme TV ce soir, direct en ce moment, chaînes TNT, films, séries, sport, documentaires et prime time : consultez rapidement ce qu\'il y a à la télé sur programmetelecesoir.fr.';
|
||||
$keywords = 'programme tv ce soir, programme télé ce soir, en ce moment, ce soir à la télé, programme tv tnt, film ce soir, série ce soir, match ce soir, émission ce soir, programme tf1, programme france 2, programme m6, programme arte, direct tv, grille tv';
|
||||
$pageTitle = 'Programme TV ce soir et en ce moment | ' . $site['domain'];
|
||||
$fallbackDescription = 'Programme TV ce soir, direct en ce moment, chaînes TNT, films, séries, sport, documentaires et prime time : consultez rapidement ce qu\'il y a à la télé sur programmetelecesoir.net.';
|
||||
$keywords = 'programme tv ce soir, programme télé ce soir, en ce moment, ce soir à la télé, programme tv tnt, film ce soir, série ce soir, match ce soir, émission ce soir, programme tf1, programme france 2, programme m6, programme arte, direct tv, grille tv, programmetelecesoir.net';
|
||||
$updatedAt = date('d/m/Y');
|
||||
$visitorStats = [
|
||||
'live' => 0,
|
||||
'daily' => 0,
|
||||
'total' => 0,
|
||||
'live_window_minutes' => 5,
|
||||
'updated_at' => date(DATE_ATOM),
|
||||
'updated_label' => date('H:i:s'),
|
||||
];
|
||||
|
||||
try {
|
||||
require_once __DIR__ . '/db/visit_counter.php';
|
||||
$visitorStats = visit_counter_snapshot();
|
||||
} catch (Throwable $exception) {
|
||||
// Le site doit rester browsable même si la base ou le compteur sont temporairement indisponibles.
|
||||
}
|
||||
|
||||
$quickLinks = [
|
||||
[
|
||||
@ -75,8 +90,8 @@ $faqItems = [
|
||||
'answer' => 'La page couvre les grandes chaînes généralistes et TNT recherchées le plus souvent : TF1, France 2, France 3, M6, Arte, Canal+, France 5, TMC, W9, C8 et d’autres chaînes populaires.',
|
||||
],
|
||||
[
|
||||
'question' => 'Quels cookies sont utilisés sur programmetelecesoir.fr ?',
|
||||
'answer' => 'Cette première version dépose un cookie essentiel pour mémoriser vos choix de consentement. Les autres fonctions optionnelles sont désactivées par défaut et peuvent être activées ou refusées depuis la bannière ou le bouton flottant.',
|
||||
'question' => 'Quels cookies sont utilisés sur programmetelecesoir.net ?',
|
||||
'answer' => 'Le site dépose le cookie essentiel ptcs_consent pour mémoriser vos choix. Si vous activez la mesure locale d\'audience, le cookie optionnel ptcs_visit_id alimente le compteur de visiteurs en direct, journalier et total, sans service tiers publicitaire.',
|
||||
],
|
||||
[
|
||||
'question' => 'Qui contacter pour les questions de confidentialité ?',
|
||||
@ -103,7 +118,7 @@ $organizationSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Organization',
|
||||
'name' => $site['domain'],
|
||||
'url' => $site['base_url'] . '/',
|
||||
'url' => ($site['canonical_base_url'] ?? $site['base_url']) . '/',
|
||||
'description' => $fallbackDescription,
|
||||
'address' => [
|
||||
'@type' => 'PostalAddress',
|
||||
@ -120,13 +135,14 @@ $organizationSchema = [
|
||||
'availableLanguage' => ['fr'],
|
||||
],
|
||||
];
|
||||
$cookieConsentLock = site_should_lock_cookie_overlay();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<?php render_site_head($pageTitle, $fallbackDescription, $keywords); ?>
|
||||
</head>
|
||||
<body class="app-body" data-page="home">
|
||||
<body class="app-body<?= $cookieConsentLock ? ' cookie-consent-locked' : '' ?>" data-page="home" data-cookie-lock="<?= $cookieConsentLock ? 'pending' : 'released' ?>">
|
||||
<?php render_site_nav('home'); ?>
|
||||
<noscript>
|
||||
<div class="container pt-3">
|
||||
@ -140,7 +156,7 @@ $organizationSchema = [
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">Programme TV • France • Mise à jour éditoriale du <?= e($updatedAt) ?></span>
|
||||
<h1>Programme TV ce soir et en ce moment : votre accès rapide aux chaînes et aux rendez-vous du soir</h1>
|
||||
<p class="lead">Programmetelecesoir.fr centralise l'intention de recherche la plus importante du secteur : savoir <strong>ce qu'il y a à la télé ce soir</strong> et <strong>ce qui passe en ce moment</strong>. La page met immédiatement le widget TV en avant, puis complète la lecture avec un guide éditorial pensé pour le prime time, les films, les séries, le sport, les documentaires et les chaînes TNT les plus consultées.</p>
|
||||
<p class="lead">Programmetelecesoir.net centralise l'intention de recherche la plus importante du secteur : savoir <strong>ce qu'il y a à la télé ce soir</strong> et <strong>ce qui passe en ce moment</strong>. La page met immédiatement le widget TV en avant, puis complète la lecture avec un guide éditorial pensé pour le prime time, les films, les séries, le sport, les documentaires et les chaînes TNT les plus consultées.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-dark btn-refined" href="#widget-section">Voir le programme en ce moment</a>
|
||||
<a class="btn btn-outline-dark btn-refined" href="#guide">Lire le guide TV du soir</a>
|
||||
@ -168,26 +184,27 @@ $organizationSchema = [
|
||||
<li><span>Audience locale</span><span class="status-pill" data-consent-badge="audience">Désactivée</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="info-panel compact-panel">
|
||||
<article class="info-panel compact-panel visit-counter-panel" data-visit-counter-endpoint="/api/visit-counter.php" data-live-window-minutes="<?= e((string) $visitorStats['live_window_minutes']) ?>" data-initial-live="<?= e((string) $visitorStats['live']) ?>" data-initial-daily="<?= e((string) $visitorStats['daily']) ?>" data-initial-total="<?= e((string) $visitorStats['total']) ?>" data-updated-at="<?= e((string) $visitorStats['updated_at']) ?>" data-updated-label="<?= e((string) $visitorStats['updated_label']) ?>">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-kicker">Mesure locale</span>
|
||||
<h2>Effet visible des toggles</h2>
|
||||
<span class="panel-kicker">Audience</span>
|
||||
<h2>Compteur visiteurs en direct</h2>
|
||||
</div>
|
||||
<p class="panel-copy" data-audience-status>La mesure locale d'audience est désactivée. Aucun service tiers n'est utilisé.</p>
|
||||
<div class="stat-grid">
|
||||
<p class="panel-copy" data-visit-status>Le compteur affiche l'audience anonyme en direct, les visiteurs du jour et le total mesuré. Il se met à jour automatiquement si la mesure locale d'audience est acceptée.</p>
|
||||
<div class="stat-grid stat-grid--counter" aria-live="polite">
|
||||
<div>
|
||||
<strong data-audience-visits>0</strong>
|
||||
<span>visites locales</span>
|
||||
<strong data-visit-live><?= e((string) $visitorStats['live']) ?></strong>
|
||||
<span>en direct</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong data-audience-sections>0</strong>
|
||||
<span>sections vues</span>
|
||||
<strong data-visit-daily><?= e((string) $visitorStats['daily']) ?></strong>
|
||||
<span>aujourd'hui</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong data-audience-last-visit>—</strong>
|
||||
<span>dernière activité</span>
|
||||
<strong data-visit-total><?= e((string) $visitorStats['total']) ?></strong>
|
||||
<span>total</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="panel-note mb-0" data-visit-updated>Dernière mise à jour : <?= e((string) $visitorStats['updated_label']) ?> · “En direct” = visiteurs actifs sur les <?= e((string) $visitorStats['live_window_minutes']) ?> dernières minutes.</p>
|
||||
</article>
|
||||
<article class="info-panel compact-panel">
|
||||
<div class="panel-heading">
|
||||
@ -241,7 +258,7 @@ $organizationSchema = [
|
||||
</div>
|
||||
<div class="content-grid">
|
||||
<article class="article-panel">
|
||||
<p>Si vous cherchez <strong>quoi regarder ce soir</strong>, la logique de programmetelecesoir.fr est simple : offrir d'abord le direct, puis un accompagnement éditorial lisible. En un seul endroit, vous pouvez surveiller le programme TV des grandes chaînes, comparer le prime time, repérer un film à la télévision ce soir, vérifier la présence d'une série, d'un documentaire, d'un match, d'une émission d'information, d'un magazine ou d'un divertissement familial.</p>
|
||||
<p>Si vous cherchez <strong>quoi regarder ce soir</strong>, la logique de programmetelecesoir.net est simple : offrir d'abord le direct, puis un accompagnement éditorial lisible. En un seul endroit, vous pouvez surveiller le programme TV des grandes chaînes, comparer le prime time, repérer un film à la télévision ce soir, vérifier la présence d'une série, d'un documentaire, d'un match, d'une émission d'information, d'un magazine ou d'un divertissement familial.</p>
|
||||
<p>La structure éditoriale de la page reprend les formulations les plus recherchées autour du <strong>programme télé ce soir</strong> : programme TV TNT ce soir, programme TV maintenant, télé ce soir, programme des chaînes en soirée, directs TV, grille TV du soir, deuxième partie de soirée, films et séries du prime time. L'objectif n'est pas d'encombrer la lecture, mais de répondre précisément aux intentions qui reviennent le plus souvent sur mobile comme sur desktop.</p>
|
||||
<p>Les internautes qui tapent <strong>programme TF1 ce soir</strong>, <strong>programme France 2 ce soir</strong>, <strong>programme M6 ce soir</strong> ou <strong>programme Arte ce soir</strong> recherchent généralement un accès immédiat, sans détour, avec une page rapide, claire et fiable. C'est pour cette raison que le site priorise la lisibilité, le widget en tête de page, des cartes chaînes synthétiques, une FAQ structurée et des documents légaux facilement accessibles.</p>
|
||||
</article>
|
||||
|
||||
@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/site.php';
|
||||
|
||||
$site = site_settings();
|
||||
$pageTitle = 'Politique de confidentialité | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
|
||||
$fallbackDescription = 'Politique de confidentialité de programmetelecesoir.fr : responsable de traitement, DPO, hébergement, données traitées et droits des personnes.';
|
||||
$keywords = 'politique de confidentialité, données personnelles, dpo, programmetelecesoir.fr';
|
||||
$pageTitle = 'Politique de confidentialité | ' . $site['domain'];
|
||||
$fallbackDescription = 'Politique de confidentialité de programmetelecesoir.net : responsable de traitement, DPO, hébergement, données traitées et droits des personnes.';
|
||||
$keywords = 'politique de confidentialité, données personnelles, dpo, programmetelecesoir.net, confidentialité programme tv';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
@ -26,7 +26,7 @@ $keywords = 'politique de confidentialité, données personnelles, dpo, programm
|
||||
</nav>
|
||||
<span class="eyebrow mt-4">Protection des données</span>
|
||||
<h1>Politique de confidentialité</h1>
|
||||
<p class="lead legal-lead">Cette politique décrit les traitements de données susceptibles d'intervenir lors de la consultation de programmetelecesoir.fr, les finalités poursuivies, les durées de conservation, les destinataires et vos droits.</p>
|
||||
<p class="lead legal-lead">Cette politique décrit les traitements de données susceptibles d'intervenir lors de la consultation de programmetelecesoir.net, les finalités poursuivies, les durées de conservation, les destinataires et vos droits.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -48,7 +48,7 @@ $keywords = 'politique de confidentialité, données personnelles, dpo, programm
|
||||
<li>les données techniques de connexion indispensables au fonctionnement du site et à la sécurité de l'infrastructure (adresse IP, user-agent, requêtes HTTP, horodatage, erreurs techniques) ;</li>
|
||||
<li>vos préférences de consentement, enregistrées dans le cookie <code>ptcs_consent</code> ;</li>
|
||||
<li>les informations échangées avec le prestataire du widget TV, nécessaires à l'affichage du service “en ce moment” lorsque vous consultez la page d'accueil ;</li>
|
||||
<li>les préférences locales optionnelles d'interface ou le compteur local d'audience, si vous activez ces catégories dans la bannière.</li>
|
||||
<li>les préférences locales optionnelles d'interface et, si vous activez la mesure d'audience, le cookie <code>ptcs_visit_id</code> ainsi que des horodatages anonymes associés au compteur visiteurs en direct, journalier et total.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Finalités et bases juridiques</h2>
|
||||
@ -78,8 +78,8 @@ $keywords = 'politique de confidentialité, données personnelles, dpo, programm
|
||||
<td>Nécessité liée au service expressément sollicité.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Personnalisation et audience locale</td>
|
||||
<td>Améliorer le confort de navigation uniquement si vous l'autorisez.</td>
|
||||
<td>Personnalisation et compteur interne de visites</td>
|
||||
<td>Améliorer le confort de navigation et afficher le compteur visiteurs en direct uniquement si vous l'autorisez.</td>
|
||||
<td>Consentement.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -87,10 +87,10 @@ $keywords = 'politique de confidentialité, données personnelles, dpo, programm
|
||||
</div>
|
||||
|
||||
<h2>5. Destinataires</h2>
|
||||
<p>Les destinataires potentiels des données techniques sont le responsable du site, l'hébergeur <strong><?= e($site['host_name']) ?></strong> pour l'exploitation de l'infrastructure et, pour le service TV embarqué, le fournisseur du widget nécessaire à l'affichage du programme.</p>
|
||||
<p>Les destinataires potentiels des données techniques sont le responsable du site, l'hébergeur <strong><?= e($site['host_name']) ?></strong> pour l'exploitation de l'infrastructure et, pour le service TV embarqué, le fournisseur du widget nécessaire à l'affichage du programme. Le compteur visiteurs optionnel reste traité en first-party dans la base locale du site, sans prestataire analytics tiers.</p>
|
||||
|
||||
<h2>6. Durées de conservation</h2>
|
||||
<p>Le cookie de consentement est conservé 6 mois. Les préférences purement locales et facultatives sont effacées si vous désactivez la catégorie correspondante. Les durées de conservation des journaux techniques dépendent des règles d'exploitation et de sécurité de l'hébergement.</p>
|
||||
<p>Le cookie de consentement et le cookie optionnel <code>ptcs_visit_id</code> sont conservés 6 mois. Les horodatages anonymes liés au compteur visiteurs sont conservés au maximum 13 mois, puis supprimés. Le total cumulé peut rester conservé sous forme agrégée, sans identifiant direct. Les durées de conservation des journaux techniques dépendent des règles d'exploitation et de sécurité de l'hébergement.</p>
|
||||
</article>
|
||||
<aside class="aside-panels">
|
||||
<article class="info-panel">
|
||||
|
||||
@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/site.php';
|
||||
|
||||
$site = site_settings();
|
||||
$pageTitle = 'Politique de cookies | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
|
||||
$fallbackDescription = 'Politique de cookies de programmetelecesoir.fr : inventaire des cookies first-party, description des préférences et informations sur le widget TV.';
|
||||
$keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr';
|
||||
$pageTitle = 'Politique de cookies | ' . $site['domain'];
|
||||
$fallbackDescription = 'Politique de cookies de programmetelecesoir.net : inventaire des cookies first-party, description des préférences et informations sur le widget TV.';
|
||||
$keywords = 'politique de cookies, consentement cookies, programmetelecesoir.net, cookies programme tv';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
@ -25,7 +25,7 @@ $keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr'
|
||||
</ol>
|
||||
</nav>
|
||||
<span class="eyebrow mt-4">Document légal</span>
|
||||
<h1>Politique de cookies de programmetelecesoir.fr</h1>
|
||||
<h1>Politique de cookies de programmetelecesoir.net</h1>
|
||||
<p class="lead legal-lead">Cette politique explique de manière détaillée quels cookies et traceurs sont utilisés sur le site, à quoi ils servent, combien de temps ils sont conservés et comment vous pouvez modifier vos préférences à tout moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,7 +37,7 @@ $keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr'
|
||||
<div class="section-heading">
|
||||
<span class="eyebrow">Inventaire</span>
|
||||
<h2>Liste des cookies maîtrisés par le site</h2>
|
||||
<p>La présente version du site limite volontairement les cookies first-party à l'essentiel. Aucun cookie publicitaire n'est mis en place par programmetelecesoir.fr.</p>
|
||||
<p>La présente version du site limite volontairement les cookies first-party au consentement, à la personnalisation locale et au compteur visiteurs interne activé sur choix. Aucun cookie publicitaire n'est mis en place par programmetelecesoir.net.</p>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table legal-table align-middle">
|
||||
@ -59,9 +59,16 @@ $keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr'
|
||||
<td>Oui</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aucun autre cookie first-party</td>
|
||||
<td><code>ptcs_visit_id</code></td>
|
||||
<td>Cookie first-party optionnel</td>
|
||||
<td>Attribuer un identifiant aléatoire afin d'alimenter le compteur visiteurs en direct, journalier et total, sans recourir à un service tiers publicitaire.</td>
|
||||
<td>6 mois</td>
|
||||
<td>Non</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aucun cookie publicitaire ou analytics tiers</td>
|
||||
<td>—</td>
|
||||
<td>La version actuelle ne déploie ni cookie publicitaire, ni cookie de retargeting, ni solution d'analyse tierce.</td>
|
||||
<td>La version actuelle ne déploie ni cookie de retargeting, ni balise marketing, ni solution d'analyse externe.</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
@ -84,19 +91,19 @@ $keywords = 'politique de cookies, consentement cookies, programmetelecesoir.fr'
|
||||
<ul class="legal-list">
|
||||
<li><strong>Essentiels :</strong> ils assurent la mémorisation de votre choix de confidentialité et le fonctionnement minimum de l'interface de consentement. Cette catégorie ne peut pas être désactivée.</li>
|
||||
<li><strong>Personnalisation :</strong> elle permet de mémoriser localement des préférences d'interface sur votre appareil, par exemple le rappel concernant le défilement horizontal du widget sur mobile.</li>
|
||||
<li><strong>Mesure locale d'audience :</strong> elle active uniquement un compteur local dans votre navigateur pour démontrer l'effet du réglage. Aucun service tiers d'analyse n'est branché dans cette version.</li>
|
||||
<li><strong>Mesure locale d'audience :</strong> elle active le cookie optionnel <code>ptcs_visit_id</code> et un compteur first-party dans la base locale du site afin d'afficher les visiteurs en direct, le nombre de visiteurs du jour et le total cumulé. Aucun service tiers d'analyse n'est branché dans cette version.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Et le widget TV externe ?</h2>
|
||||
<p>Le service principal du site consiste à afficher un programme TV “en ce moment”. Le widget TV est donc traité comme un élément nécessaire à la prestation expressément attendue sur la page d'accueil. Il est chargé depuis un service tiers, <strong>tv-programme.com</strong>, afin d'afficher les grilles et contenus correspondants. Lorsqu'un service tiers est sollicité, il peut techniquement recevoir certaines informations de connexion liées à votre navigateur. Pour connaître les règles exactes applicables à ce prestataire, il convient également de consulter sa propre documentation.</p>
|
||||
|
||||
<h2>4. Comment modifier vos choix plus tard ?</h2>
|
||||
<p>Un bouton flottant en bas à gauche de l'écran permet de rouvrir à tout moment le centre de préférences. Les toggles sont resynchronisés avec votre choix enregistré afin de vous permettre de l'ajuster immédiatement, sans rechargement complexe ni perte de navigation.</p>
|
||||
<p>Un bouton flottant en bas à gauche de l'écran permet de rouvrir à tout moment le centre de préférences. Les toggles sont resynchronisés avec votre choix enregistré afin de vous permettre de l'ajuster immédiatement, sans rechargement complexe ni perte de navigation. Si vous retirez votre consentement pour la mesure d'audience, le cookie <code>ptcs_visit_id</code> est supprimé et la visite en cours n'est plus alimentée dans le compteur.</p>
|
||||
</article>
|
||||
<aside class="aside-panels">
|
||||
<article class="info-panel">
|
||||
<h3>Durée de conservation</h3>
|
||||
<p>Le cookie de consentement est conservé pendant <strong>6 mois</strong>, puis vos préférences peuvent être redemandées. Les traces locales purement optionnelles sont effacées lorsque vous désactivez la catégorie correspondante.</p>
|
||||
<p>Le cookie de consentement et le cookie optionnel de visite sont conservés pendant <strong>6 mois</strong>. Les horodatages anonymes associés au compteur interne sont conservés au maximum <strong>13 mois</strong>, tandis que le total cumulé affiché peut être maintenu sous forme agrégée sans identifiant direct.</p>
|
||||
</article>
|
||||
<article class="info-panel">
|
||||
<h3>Base légale</h3>
|
||||
|
||||
@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/site.php';
|
||||
|
||||
$site = site_settings();
|
||||
$pageTitle = 'Règlement d\'utilisation | ' . ($site['project_name'] !== '' ? $site['project_name'] : $site['domain']);
|
||||
$fallbackDescription = 'Règlement d\'utilisation de programmetelecesoir.fr : accès au site, propriété intellectuelle, responsabilité, disponibilité et règles de consultation.';
|
||||
$keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions essentielles';
|
||||
$pageTitle = 'Règlement d\'utilisation | ' . $site['domain'];
|
||||
$fallbackDescription = 'Règlement d\'utilisation de programmetelecesoir.net : accès au site, propriété intellectuelle, responsabilité, disponibilité et règles de consultation.';
|
||||
$keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions essentielles, programmetelecesoir.net';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
@ -26,7 +26,7 @@ $keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions esse
|
||||
</nav>
|
||||
<span class="eyebrow mt-4">Cadre d'usage</span>
|
||||
<h1>Règlement d'utilisation du site</h1>
|
||||
<p class="lead legal-lead">Le présent règlement encadre l'utilisation de programmetelecesoir.fr, précise l'objet du service, les responsabilités respectives, les règles de disponibilité et les bonnes pratiques applicables à la consultation du contenu.</p>
|
||||
<p class="lead legal-lead">Le présent règlement encadre l'utilisation de programmetelecesoir.net, précise l'objet du service, les responsabilités respectives, les règles de disponibilité et les bonnes pratiques applicables à la consultation du contenu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -37,13 +37,13 @@ $keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions esse
|
||||
<div class="content-grid legal-grid">
|
||||
<article class="article-panel">
|
||||
<h2>1. Objet du site</h2>
|
||||
<p>Programmetelecesoir.fr est une page éditoriale conçue pour faciliter l'accès au programme TV ce soir et au direct en ce moment. Le site met en avant un widget externe nécessaire au service principal, complété par des contenus éditoriaux, des sections d'aide et des documents juridiques.</p>
|
||||
<p>Programmetelecesoir.net est une page éditoriale conçue pour faciliter l'accès au programme TV ce soir et au direct en ce moment. Le site met en avant un widget externe nécessaire au service principal, complété par des contenus éditoriaux, des sections d'aide et des documents juridiques.</p>
|
||||
|
||||
<h2>2. Conditions d'accès</h2>
|
||||
<p>L'accès au site est libre, sous réserve d'une connexion internet compatible. L'utilisateur s'engage à ne pas détourner l'usage du site, à ne pas perturber son bon fonctionnement et à respecter les lois et règlements applicables lors de sa navigation.</p>
|
||||
|
||||
<h2>3. Propriété intellectuelle</h2>
|
||||
<p>Les textes, compositions, éléments graphiques, structures de mise en page et contenus créés pour programmetelecesoir.fr sont protégés par les règles applicables à la propriété intellectuelle. Les marques, logos, titres d'émissions, programmes ou contenus tiers restent la propriété de leurs ayants droit respectifs.</p>
|
||||
<p>Les textes, compositions, éléments graphiques, structures de mise en page et contenus créés pour programmetelecesoir.net sont protégés par les règles applicables à la propriété intellectuelle. Les marques, logos, titres d'émissions, programmes ou contenus tiers restent la propriété de leurs ayants droit respectifs.</p>
|
||||
|
||||
<h2>4. Services tiers et sources externes</h2>
|
||||
<p>Le site s'appuie sur un widget TV embarqué fourni par un service externe afin d'afficher l'information “en ce moment”. La disponibilité, l'exactitude et l'éventuelle évolution de ce service tiers relèvent également de son éditeur. Le site présente clairement la source du widget au sein de la page d'accueil.</p>
|
||||
@ -55,7 +55,7 @@ $keywords = 'règlement d\'utilisation, conditions d\'utilisation, mentions esse
|
||||
<p>Le site a une vocation d'information et d'accès rapide. L'utilisateur reste libre de vérifier le programme final auprès des diffuseurs concernés. Le responsable ne pourra être tenu responsable d'un changement de grille, d'une annulation de programme, d'une erreur provenant d'une source tierce ou d'une indisponibilité ponctuelle indépendante de sa volonté.</p>
|
||||
|
||||
<h2>7. Données personnelles et cookies</h2>
|
||||
<p>L'utilisation du site emporte l'application des documents dédiés à la confidentialité et aux cookies. La bannière de consentement, le bouton flottant de rappel et les pages légales sont conçus pour permettre à l'utilisateur de comprendre et d'ajuster ses choix simplement.</p>
|
||||
<p>L'utilisation du site emporte l'application des documents dédiés à la confidentialité et aux cookies. La bannière de consentement, le bouton flottant de rappel, le compteur visiteurs optionnel et les pages légales sont conçus pour permettre à l'utilisateur de comprendre et d'ajuster ses choix simplement.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>Pour toute question relative au fonctionnement du site, à la confidentialité ou à l'exercice de vos droits, vous pouvez contacter le DPO déclaré : <strong><?= e($site['dpo_name']) ?></strong>, <?= e($site['dpo_address']) ?>, tél. <?= e($site['dpo_phone']) ?>.</p>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://programmetelecesoir.fr/sitemap.xml
|
||||
Sitemap: https://programmetelecesoir.net/sitemap.xml
|
||||
|
||||
81
site.php
81
site.php
@ -33,14 +33,23 @@ function site_settings(): array
|
||||
}
|
||||
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'programmetelecesoir.fr';
|
||||
$canonicalDomain = 'programmetelecesoir.net';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? $canonicalDomain;
|
||||
$projectName = (string) ($_SERVER['PROJECT_NAME'] ?? '');
|
||||
if ($projectName === '' || strcasecmp($projectName, 'programmetelecesoir.fr') === 0) {
|
||||
$projectName = $canonicalDomain;
|
||||
} else {
|
||||
$projectName = str_ireplace('programmetelecesoir.fr', $canonicalDomain, $projectName);
|
||||
}
|
||||
$projectDescription = str_ireplace('programmetelecesoir.fr', $canonicalDomain, (string) ($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
|
||||
|
||||
$settings = [
|
||||
'domain' => 'programmetelecesoir.fr',
|
||||
'project_name' => $_SERVER['PROJECT_NAME'] ?? 'programmetelecesoir.fr',
|
||||
'project_description' => $_SERVER['PROJECT_DESCRIPTION'] ?? '',
|
||||
'domain' => $canonicalDomain,
|
||||
'project_name' => $projectName,
|
||||
'project_description' => $projectDescription,
|
||||
'project_image_url' => $_SERVER['PROJECT_IMAGE_URL'] ?? '',
|
||||
'base_url' => $scheme . '://' . $host,
|
||||
'canonical_base_url' => 'https://' . $canonicalDomain,
|
||||
'asset_version' => site_asset_version(),
|
||||
'owner_name' => 'M LORENTE CHRISTOPHE',
|
||||
'dpo_name' => 'M LORENTE CHRISTOPHE',
|
||||
@ -61,7 +70,11 @@ function site_current_path(): string
|
||||
{
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$path = strtok($requestUri, '?');
|
||||
return $path !== false && $path !== '' ? $path : '/';
|
||||
if ($path === false || $path === '' || $path === '/index.php') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
function site_asset_url(string $relativePath): string
|
||||
@ -69,25 +82,41 @@ function site_asset_url(string $relativePath): string
|
||||
return $relativePath . '?v=' . rawurlencode(site_asset_version());
|
||||
}
|
||||
|
||||
function site_has_saved_consent(): bool
|
||||
{
|
||||
$rawConsent = $_COOKIE['ptcs_consent'] ?? null;
|
||||
if (!is_string($rawConsent) || $rawConsent === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$decoded = json_decode($rawConsent, true);
|
||||
return is_array($decoded) && array_key_exists('essential', $decoded);
|
||||
}
|
||||
|
||||
function site_is_home_request(): bool
|
||||
{
|
||||
return in_array(site_current_path(), ['/', '/index.php'], true);
|
||||
}
|
||||
|
||||
function site_should_lock_cookie_overlay(): bool
|
||||
{
|
||||
return site_is_home_request() && !site_has_saved_consent();
|
||||
}
|
||||
|
||||
function render_site_head(string $pageTitle, string $fallbackDescription, string $keywords = '', bool $noindex = false): void
|
||||
{
|
||||
$site = site_settings();
|
||||
$projectDescription = $site['project_description'];
|
||||
$projectImageUrl = $site['project_image_url'];
|
||||
$canonical = $site['base_url'] . site_current_path();
|
||||
$canonical = ($site['canonical_base_url'] ?? $site['base_url']) . site_current_path();
|
||||
$description = $fallbackDescription !== '' ? $fallbackDescription : $projectDescription;
|
||||
?>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><?= e($pageTitle) ?></title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content="<?= e($projectDescription) ?>" />
|
||||
<meta property="og:description" content="<?= e($projectDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= e($projectDescription) ?>" />
|
||||
<?php else: ?>
|
||||
<meta name="description" content="<?= e($fallbackDescription) ?>" />
|
||||
<meta property="og:description" content="<?= e($fallbackDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= e($fallbackDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta name="description" content="<?= e($description) ?>" />
|
||||
<meta property="og:description" content="<?= e($description) ?>" />
|
||||
<meta property="twitter:description" content="<?= e($description) ?>" />
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= e($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>" />
|
||||
@ -97,7 +126,9 @@ function render_site_head(string $pageTitle, string $fallbackDescription, string
|
||||
<?php endif; ?>
|
||||
<meta name="author" content="<?= e($site['owner_name']) ?>" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta property="og:site_name" content="<?= e($site['domain']) ?>" />
|
||||
<meta property="og:title" content="<?= e($pageTitle) ?>" />
|
||||
<meta name="twitter:title" content="<?= e($pageTitle) ?>" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="<?= e($canonical) ?>" />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
@ -127,11 +158,12 @@ function nav_link(string $label, string $href, bool $active = false, bool $secti
|
||||
|
||||
function render_site_nav(string $current = 'home'): void
|
||||
{
|
||||
$site = site_settings();
|
||||
?>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">programmetelecesoir.fr</a>
|
||||
<a class="navbar-brand" href="/"><?= e($site['domain']) ?></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Afficher la navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@ -160,7 +192,7 @@ function render_site_footer(): void
|
||||
<div class="container">
|
||||
<div class="footer-grid">
|
||||
<div>
|
||||
<div class="footer-brand">programmetelecesoir.fr</div>
|
||||
<div class="footer-brand"><?= e($site['domain']) ?></div>
|
||||
<p class="footer-copy">Guide éditorial et page d'accès rapide au programme TV ce soir, au direct en ce moment et aux principales chaînes TNT.</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -192,22 +224,27 @@ function render_site_footer(): void
|
||||
|
||||
function render_cookie_controls(): void
|
||||
{
|
||||
$hasSavedConsent = site_has_saved_consent();
|
||||
$showBanner = !$hasSavedConsent;
|
||||
$lockHome = site_should_lock_cookie_overlay();
|
||||
?>
|
||||
<div class="cookie-floating">
|
||||
<div class="cookie-floating"<?= $showBanner ? ' hidden' : '' ?>>
|
||||
<button type="button" class="cookie-reopen" id="cookie-reopen" aria-controls="cookie-banner" aria-expanded="false">
|
||||
<span class="cookie-reopen__dot" aria-hidden="true"></span>
|
||||
<span id="cookie-reopen-label">Cookies : essentiels</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner" id="cookie-banner" hidden>
|
||||
<div class="cookie-overlay" id="cookie-overlay" aria-hidden="true"<?= $lockHome ? '' : ' hidden' ?>></div>
|
||||
|
||||
<div class="cookie-banner<?= $lockHome ? ' cookie-banner--modal' : '' ?>" id="cookie-banner" role="dialog" aria-modal="<?= $lockHome ? 'true' : 'false' ?>" aria-labelledby="cookie-banner-title" aria-describedby="cookie-banner-desc" tabindex="-1"<?= $showBanner ? '' : ' hidden' ?>>
|
||||
<div class="cookie-banner__top">
|
||||
<div>
|
||||
<p class="cookie-banner__eyebrow mb-2">Préférences cookies</p>
|
||||
<h2 class="cookie-banner__title">Gérez votre confidentialité</h2>
|
||||
<h2 class="cookie-banner__title" id="cookie-banner-title">Gérez votre confidentialité</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cookie-banner__intro">Le widget TV nécessaire au service reste actif pour afficher le programme en temps réel. Les traceurs optionnels sont désactivés par défaut tant que vous n'avez pas choisi.</p>
|
||||
<p class="cookie-banner__intro" id="cookie-banner-desc">Le widget TV nécessaire au service reste actif pour afficher le programme en temps réel. Les traceurs optionnels sont désactivés par défaut tant que vous n'avez pas choisi. Si vous activez la mesure locale d'audience, un identifiant first-party anonyme alimente le compteur visiteurs en direct, journalier et total. Sur la page d'accueil, l'overlay bloque la navigation jusqu'à l'enregistrement d'un choix.</p>
|
||||
<div class="cookie-banner__toggles">
|
||||
<div class="cookie-switch-row">
|
||||
<div>
|
||||
@ -230,7 +267,7 @@ function render_cookie_controls(): void
|
||||
<div class="cookie-switch-row">
|
||||
<div>
|
||||
<label class="cookie-switch-label" for="cookie-audience">Mesure locale d'audience</label>
|
||||
<p>Active un simple compteur local dans votre navigateur, sans service tiers ni publicité.</p>
|
||||
<p>Active un compteur first-party anonyme pour afficher les visiteurs en direct, les visiteurs du jour et le total, sans service tiers ni publicité.</p>
|
||||
</div>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="cookie-audience" data-consent-control="audience">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://programmetelecesoir.fr/</loc>
|
||||
<loc>https://programmetelecesoir.net/</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user