texte), yb_parcours, yb_skills, yb_services (listes). */ /** Valeurs par défaut (= contenu de référence du design v3). */ function yb2026_profile_defaults() { return array( 'yb_identity' => array( 'poste' => 'Chargé support logiciel MCO', 'entreprise' => 'SeipraScore', 'casquettes' => 'Développeur web · Admin sys & réseaux', 'localisation' => 'Région lyonnaise', 'region' => 'Auvergne-Rhône-Alpes', 'ville_schema' => 'Lyon', 'email' => 'contact@exemple.fr', 'telephone' => '06 00 00 00 00', 'mobilite' => 'Permis B', 'hero_texte' => 'Support logiciel et maintien en conditions opérationnelles chez SeipraScore. Développeur web et admin Linux à côté, en continu.', 'apropos' => '', 'intro' => '', 'accroche' => 'Un projet ? Une question ? Un échange ?', ), 'yb_parcours' => array(), 'yb_skills' => array(), 'yb_services' => array(), ); } /** Description des champs de chaque répéteur : clé => [libellé, type]. */ function yb2026_profile_repeaters() { return array( 'yb_parcours' => array( 'title' => 'Parcours', 'help' => 'Du plus récent au plus ancien. La date et le lieu s\'affichent en majuscules automatiquement.', 'fields' => array('date' => array('Période', 'text'), 'titre' => array('Intitulé', 'text'), 'lieu' => array('Lieu / établissement', 'text'), 'desc' => array('Description', 'textarea')), ), 'yb_skills' => array( 'title' => 'Compétences techniques', 'help' => 'Barres de la page Qui suis-je. Pourcentage = remplissage de la barre (0 à 100).', 'fields' => array('name' => array('Compétence', 'text'), 'level' => array('Niveau', 'text'), 'pct' => array('%', 'number')), ), 'yb_services' => array( 'title' => 'Services (page Compétences)', 'help' => 'Cartes numérotées ( 01 ), ( 02 )… dans l\'ordre de la liste.', 'fields' => array('titre' => array('Titre', 'text'), 'desc' => array('Description', 'textarea')), ), ); } /** Libellés des champs d'identité. */ function yb2026_identity_fields() { return array( 'poste' => array('Poste', 'text', 'Kicker du hero, fiche Qui suis-je, données structurées.'), 'entreprise' => array('Entreprise', 'text', 'Badge de la nav (« @ ENTREPRISE »), hero, schema.org worksFor.'), 'casquettes' => array('Casquettes', 'text', 'Sous-titre du hero, avant la localisation.'), 'localisation' => array('Localisation', 'text', 'Volontairement large : pas de ville précise.'), 'region' => array('Région', 'text', 'Ajoutée après la localisation dans le footer, le menu mobile et le contact.'), 'ville_schema' => array('Ville (SEO uniquement)', 'text', 'Utilisée seulement dans les données structurées schema.org, jamais affichée.'), 'email' => array('E-mail', 'text', ''), 'telephone' => array('Téléphone', 'text', ''), 'mobilite' => array('Mobilité', 'text', ''), 'hero_texte' => array('Texte du hero (à droite)', 'textarea', 'Accueil, paragraphe au-dessus de « SCROLL TO EXPLORE ».'), 'apropos' => array('Texte « À propos » de l\'accueil', 'textarea', 'Section CODE. SERVEURS. RÉSEAUX. Les mots entre *astérisques* passent en lime.'), 'intro' => array('Intro de la page Qui suis-je', 'textarea', ''), 'accroche' => array('Accroche du bloc contact', 'text', 'Au-dessus de « TRAVAILLONS ENSEMBLE ».'), ); } /** Lecture d'une option du profil, fusionnée avec les valeurs par défaut. */ function yb2026_profile($option) { $defaults = yb2026_profile_defaults(); $value = get_option($option, null); if ($option === 'yb_identity') { return array_merge($defaults['yb_identity'], is_array($value) ? array_filter($value, 'strlen') : array()); } return is_array($value) ? $value : $defaults[$option]; } /** Raccourci : un champ d'identité. */ function yb2026_id($key) { $id = yb2026_profile('yb_identity'); return isset($id[$key]) ? $id[$key] : ''; } /** Localisation complète : « Région lyonnaise, Auvergne-Rhône-Alpes ». */ function yb2026_location_long() { $loc = yb2026_id('localisation'); $reg = yb2026_id('region'); return $reg ? $loc . ', ' . $reg : $loc; } /** « 06 00 00 00 00 » → « tel:+33600000000 ». */ function yb2026_tel_href($tel) { $digits = preg_replace('/\D/', '', (string) $tel); return 'tel:' . (strpos($digits, '0') === 0 ? '+33' . substr($digits, 1) : '+' . $digits); } /** Texte avec *mots* mis en lime (échappé). */ function yb2026_lime_marks($text) { return preg_replace('/\*([^*]+)\*/u', '$1', esc_html($text)); } /* ── Admin ──────────────────────────────────────────────────────────── */ add_action('admin_menu', function () { add_menu_page('Profil', 'Profil', 'edit_pages', 'yb-profil', 'yb2026_profile_page', 'dashicons-id-alt', 3); }); function yb2026_profile_page() { $identity = yb2026_profile('yb_identity'); ?>

Profil

Ces informations alimentent tout le site : hero, nav, Qui suis-je, Compétences, Contact, footer et données SEO.

Profil enregistré.

Situation et coordonnées

$f) : ?> $rep) : $rows = yb2026_profile($opt); ?>

$row) yb2026_profile_row($opt, $rep['fields'], $i, $row); ?>

). */ function yb2026_profile_row($opt, $fields, $i, $row) { echo ''; foreach ($fields as $key => $f) { $name = $opt . '[' . $i . '][' . $key . ']'; $val = isset($row[$key]) ? $row[$key] : ''; echo ''; if ($f[1] === 'textarea') { echo ''; } else { $extra = $f[1] === 'number' ? ' min="0" max="100"' : ''; echo ''; } echo ''; } echo '' . ' ' . ' ' . '' . ''; } add_action('admin_post_yb_save_profile', function () { if (!current_user_can('edit_pages')) wp_die('Accès refusé.'); check_admin_referer('yb_save_profile'); $identity = array(); $raw = isset($_POST['yb_identity']) ? (array) wp_unslash($_POST['yb_identity']) : array(); foreach (yb2026_identity_fields() as $key => $f) { $val = isset($raw[$key]) ? $raw[$key] : ''; $identity[$key] = $f[1] === 'textarea' ? sanitize_textarea_field($val) : sanitize_text_field($val); } update_option('yb_identity', $identity, false); foreach (yb2026_profile_repeaters() as $opt => $rep) { $rows = isset($_POST[$opt]) ? (array) wp_unslash($_POST[$opt]) : array(); $clean = array(); foreach ($rows as $row) { $item = array(); foreach ($rep['fields'] as $key => $f) { $val = isset($row[$key]) ? $row[$key] : ''; if ($f[1] === 'number') $item[$key] = max(0, min(100, (int) $val)); elseif ($f[1] === 'textarea') $item[$key] = sanitize_textarea_field($val); else $item[$key] = sanitize_text_field($val); } /* Ligne vide ignorée */ if (implode('', array_map('strval', array_filter($item, 'is_string')))) $clean[] = $item; } update_option($opt, $clean, false); } wp_safe_redirect(admin_url('admin.php?page=yb-profil&updated=1')); exit; }); /* ── Données structurées schema.org Person ───────────────────────────── SEOPress émet déjà un Person minimal avec le même @id : les deux nœuds fusionnent, celui-ci ajoute poste, employeur et zone géographique. */ add_action('wp_head', function () { $id = yb2026_profile('yb_identity'); $person = array( '@context' => 'https://schema.org', '@type' => 'Person', '@id' => home_url('/#person'), 'name' => 'Yasser Boulayoune', 'url' => home_url('/'), 'jobTitle' => $id['poste'], 'worksFor' => array('@type' => 'Organization', 'name' => $id['entreprise']), 'address' => array('@type' => 'PostalAddress', 'addressLocality' => $id['ville_schema'], 'addressRegion' => $id['region'], 'addressCountry' => 'FR'), 'knowsAbout' => array('Support logiciel', 'MCO', 'Développement web', 'Administration systèmes et réseaux', 'Linux', 'Docker', 'WordPress', 'Symfony'), ); echo '' . "\n"; }, 20); /* ── Protection anti-collecte de l'email et du téléphone ─────────────── Dans le HTML, ces valeurs n'apparaissent jamais en clair : elles sont encodées (ROT13 → inversion → base64) et décodées en JS au premier geste d'un visiteur humain (voir YBProtect dans effects.js). Un robot qui lit le HTML brut ne trouve ni « @ », ni « mailto: », ni numéro. */ /** Encode une chaîne (décodage JS : atob → inversion → ROT13). */ function yb2026_obf($s) { return base64_encode(strrev(str_rot13((string) $s))); } /** Texte protégé : affiché « •••• » tant que le JS ne l'a pas décodé. */ function yb2026_pv($value) { return '' . str_repeat('•', 8) . ''; } /** Attributs d'un lien protégé (href décodé au premier geste). */ function yb2026_pv_href($href) { return 'href="#" data-pv-href="' . esc_attr(yb2026_obf($href)) . '"'; } /** Contenu des pages (mentions légales…) : email et téléphone du profil protégés automatiquement. */ add_filter('the_content', function ($html) { $email = yb2026_id('email'); $tel = yb2026_id('telephone'); if ($email) { /* x → lien protégé */ $html = preg_replace_callback('#]*?)href=(["\'])mailto:' . preg_quote($email, '#') . '\2([^>]*)>(.*?)#is', function ($m) use ($email) { $inner = str_ireplace($email, yb2026_pv($email), $m[4]); return '' . $inner . ''; }, $html); /* Email en texte brut restant (hors attributs) */ $html = preg_replace_callback('#(>[^<]*?)' . preg_quote($email, '#') . '#i', function ($m) use ($email) { return $m[1] . yb2026_pv($email); }, $html); } if ($tel) { $digits = preg_replace('/\D/', '', $tel); $loose = implode('[\s.\-]?', str_split($digits)); // « 06 00 00 00 00 », « 06.00… », « 0600000000 » $html = preg_replace_callback('#(>[^<]*?)' . $loose . '#', function ($m) use ($tel) { return $m[1] . yb2026_pv($tel); }, $html); } return $html; }, 30);