PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC ]); }catch(Exception $e){ http_response_code(500); die("DB error: ".$e->getMessage()); } } /* ===== Helpers ===== */ function base_url_prefix(){ $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); return $proto.$host.$base; } function normalize_media_url($src){ $src = trim((string)$src); if ($src === '') return ''; if (preg_match('#^https?://#i', $src)) return $src; if (preg_match('#^/?uploads/#i', $src)) { $prefix = base_url_prefix(); $src = ltrim($src, '/'); return rtrim($prefix,'/').'/'.$src; } return rtrim(base_url_prefix(),'/').'/'.ltrim($src,'/'); } function is_youtube($u){ return (bool)preg_match('#(youtube\.com|youtu\.be)#i',$u); } function yt_id($u){ if (preg_match('#youtu\.be/([^?&/]+)#i', $u, $m)) return $m[1]; if (preg_match('#v=([^&]+)#i', $u, $m)) return $m[1]; if (preg_match('#/embed/([^?&/]+)#i', $u, $m)) return $m[1]; return ''; } function is_vimeo($u){ return (bool)preg_match('#vimeo\.com#i',$u); } function vimeo_id($u){ if (preg_match('#vimeo\.com/(\d+)#i', $u, $m)) return $m[1]; if (preg_match('#player\.vimeo\.com/video/(\d+)#i', $u, $m)) return $m[1]; return ''; } function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function render_block($b){ $type = $b['type'] ?? ''; switch ($type) { case 'h1': return '
'.($b['content'] ?? '').'
'; case 'quote': return ''.($b['content'] ?? '').''; case 'link': { $href = e($b['href'] ?? '#'); $txt = $b['content'] ?? $href; return ''; } case 'img': { $src = normalize_media_url($b['src'] ?? ''); if (!$src) return ''; return '