yb2026/functions.php

291 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* yb2026 : thème portfolio « sombre & premium »
* v3 : porté depuis handoff/reference/portfolio-site-v3.html (+ theme-assets/)
*/
/* ── Assets ─────────────────────────────────────────────────────────── */
function yb2026_assets()
{
$v = '3.0';
$uri = get_template_directory_uri();
/* Polices hébergées sur le serveur (plus d'appel à Google Fonts : pas de transfert d'IP des visiteurs) */
wp_enqueue_style('yb-fonts', $uri . '/assets/fonts/fonts.css', array(), '1');
wp_enqueue_style('yb-effects', $uri . '/assets/css/effects.css', array(), $v);
wp_enqueue_style('yb-style', $uri . '/assets/css/style.css', array('yb-effects'), $v);
/* Ordre : cursor + orb définissent YBCursor/YBOrb, effects.js les initialise */
wp_enqueue_script('yb-cursor', $uri . '/assets/js/cursor.js', array(), $v, true);
if (is_front_page()) {
wp_enqueue_script('yb-orb', $uri . '/assets/js/orb.js', array(), $v, true);
}
wp_enqueue_script('yb-effects', $uri . '/assets/js/effects.js', array('yb-cursor'), $v, true);
if (is_page('qui-suis-je')) {
wp_enqueue_script('yb-about', $uri . '/assets/js/about.js', array('yb-effects'), $v . '.1', true);
}
if (is_page('competences')) {
wp_enqueue_script('yb-competences', $uri . '/assets/js/competences.js', array('yb-effects'), $v . '.1', true);
}
}
add_action('wp_enqueue_scripts', 'yb2026_assets');
require_once get_template_directory() . '/inc/profile.php';
require_once get_template_directory() . '/inc/proofs.php';
require_once get_template_directory() . '/inc/seasons.php';
require_once get_template_directory() . '/inc/errors.php';
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', array('style', 'script'));
/* ── Custom Post Types (repris du thème précédent, mêmes rewrites) ──── */
function yb2026_post_types()
{
register_post_type('realisation', array(
'labels' => array(
'name' => 'Réalisations',
'singular_name' => 'Réalisation',
'add_new_item' => 'Ajouter une réalisation',
'edit_item' => 'Modifier la réalisation',
),
'public' => true,
'has_archive' => false,
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true,
'rewrite' => array('slug' => 'realisations'),
));
register_post_type('service', array(
'labels' => array(
'name' => 'Services',
'singular_name' => 'Service',
'add_new_item' => 'Ajouter un service',
'edit_item' => 'Modifier le service',
),
'public' => true,
'has_archive' => false,
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true,
'rewrite' => array('slug' => 'services'),
));
}
add_action('init', 'yb2026_post_types');
function yb2026_flush()
{
yb2026_post_types();
flush_rewrite_rules();
}
add_action('after_switch_theme', 'yb2026_flush');
/* ── Meta-box réalisation : champs de l'étude de cas ────────────────── */
function yb2026_add_metaboxes()
{
add_meta_box('yb_real_meta', 'Étude de cas (design v2)', 'yb2026_render_real_metabox', 'realisation', 'normal', 'high');
}
add_action('add_meta_boxes', 'yb2026_add_metaboxes');
function yb2026_render_real_metabox($post)
{
wp_nonce_field('yb_real_nonce_action', 'yb_real_nonce');
$f = function ($k) use ($post) { return esc_attr(get_post_meta($post->ID, $k, true)); };
$sections = get_post_meta($post->ID, '_yb_sections', true);
$sections_txt = '';
if ($sections) {
$arr = json_decode($sections, true);
if (is_array($arr)) {
foreach ($arr as $s) {
$sections_txt .= '/ ' . $s['h'] . "\n" . $s['p'] . "\n\n";
}
}
}
?>
<style>.yb-mb label{font-weight:600;display:block;margin:12px 0 4px}.yb-mb input,.yb-mb textarea{width:100%}</style>
<div class="yb-mb">
<label for="yb_order">Ordre (01–09, utilisé pour la numérotation et le tri)</label>
<input type="number" id="yb_order" name="yb_order" value="<?php echo $f('_yb_order'); ?>" min="1" max="99">
<label for="yb_tag">Stack / tag (affiché en mono lime, ex. « DEBIAN 12 · DOCKER · WIREGUARD »)</label>
<input type="text" id="yb_tag" name="yb_tag" value="<?php echo $f('_yb_tag'); ?>">
<label for="yb_year">Année (ex. « 2024–2026 »)</label>
<input type="text" id="yb_year" name="yb_year" value="<?php echo $f('_yb_year'); ?>">
<label for="yb_role">Rôle (ex. « Admin Sys & Réseaux »)</label>
<input type="text" id="yb_role" name="yb_role" value="<?php echo $f('_yb_role'); ?>">
<label for="yb_short">Description courte (cartes)</label>
<input type="text" id="yb_short" name="yb_short" value="<?php echo $f('_yb_short'); ?>">
<label for="yb_desc">Description longue (intro de l'étude de cas)</label>
<textarea id="yb_desc" name="yb_desc" rows="3"><?php echo esc_textarea(get_post_meta($post->ID, '_yb_desc', true)); ?></textarea>
<label for="yb_note">Note terminal (bordure lime, ex. « $ uptime · en production depuis 2 ans »)</label>
<input type="text" id="yb_note" name="yb_note" value="<?php echo $f('_yb_note'); ?>">
<label for="yb_sections">Sections de l'étude de cas : format : ligne « / Titre » puis paragraphe, sections séparées par une ligne vide. Laisser vide pour afficher le contenu de l'éditeur à la place.</label>
<textarea id="yb_sections" name="yb_sections" rows="12" placeholder="/ Le point de départ&#10;Texte du paragraphe…&#10;&#10;/ L'architecture&#10;Texte…"><?php echo esc_textarea($sections_txt); ?></textarea>
</div>
<?php
}
function yb2026_save_metaboxes($post_id)
{
if (
!isset($_POST['yb_real_nonce']) ||
!wp_verify_nonce($_POST['yb_real_nonce'], 'yb_real_nonce_action') ||
!current_user_can('edit_post', $post_id)
) return;
foreach (array('yb_order' => '_yb_order', 'yb_tag' => '_yb_tag', 'yb_year' => '_yb_year', 'yb_role' => '_yb_role', 'yb_short' => '_yb_short', 'yb_note' => '_yb_note') as $field => $key) {
if (isset($_POST[$field])) {
update_post_meta($post_id, $key, sanitize_text_field($_POST[$field]));
}
}
if (isset($_POST['yb_desc'])) {
update_post_meta($post_id, '_yb_desc', sanitize_textarea_field($_POST['yb_desc']));
}
if (isset($_POST['yb_sections'])) {
$raw = str_replace("\r\n", "\n", (string) $_POST['yb_sections']);
$out = array();
foreach (preg_split('/\n\s*\n/', trim($raw)) as $block) {
$lines = explode("\n", trim($block));
if (count($lines) < 2 || strpos($lines[0], '/') !== 0) continue;
$out[] = array(
'h' => sanitize_text_field(ltrim(substr($lines[0], 1))),
'p' => sanitize_textarea_field(implode(' ', array_slice($lines, 1))),
);
}
update_post_meta($post_id, '_yb_sections', $out ? wp_json_encode($out, JSON_UNESCAPED_UNICODE) : '');
}
}
add_action('save_post_realisation', 'yb2026_save_metaboxes');
/* ── Helpers ─────────────────────────────────────────────────────────── */
/** Toutes les réalisations triées par _yb_order (fallback date). */
function yb2026_get_projects($limit = -1)
{
$q = new WP_Query(array(
'post_type' => 'realisation',
'posts_per_page' => $limit,
'meta_key' => '_yb_order',
'orderby' => array('meta_value_num' => 'ASC', 'date' => 'DESC'),
'no_found_rows' => true,
));
return $q->posts;
}
/** Numéro « 01 » d'une réalisation. */
function yb2026_num($post_id)
{
$o = (int) get_post_meta($post_id, '_yb_order', true);
return str_pad($o ? $o : 1, 2, '0', STR_PAD_LEFT);
}
/** Nom court d'un projet : partie avant le « : » du titre WP (ancien séparateur « : » toléré). */
function yb2026_short_title($p)
{
$t = get_the_title($p);
$parts = preg_split('/\s+[:\x{2014}]\s+/u', $t, 2);
return $parts[0];
}
/** Carte projet (piste des travaux sur l'accueil + grille réalisations).
Classes .card / .card-img attendues par effects.css et effects.js (compteur, hover N&B). */
function yb2026_project_card($p, $home = false)
{
$tag = get_post_meta($p->ID, '_yb_tag', true);
if (!$tag) $tag = strtoupper((string) get_post_meta($p->ID, '_real_technologies', true));
$short = get_post_meta($p->ID, '_yb_short', true);
if (!$short) $short = get_the_excerpt($p);
$year = get_post_meta($p->ID, '_yb_year', true);
$name = yb2026_short_title($p);
?>
<a class="card<?php echo $home ? ' card--home' : ''; ?>" href="<?php echo esc_url(get_permalink($p)); ?>" data-cursor-label="VOIR LE PROJET" data-wipe="<?php echo esc_attr(mb_strtoupper($name)); ?>"<?php echo $home ? '' : ' data-reveal'; ?>>
<span class="card-img">
<?php echo get_the_post_thumbnail($p, 'large', array('loading' => 'lazy', 'alt' => esc_attr($name))); ?>
<span class="card-num"><?php echo esc_html(yb2026_num($p->ID)); ?></span>
<?php if ($home && $year) : ?><span class="card-year"><?php echo esc_html($year); ?></span><?php endif; ?>
</span>
<span class="card-body">
<span class="card-head"><span class="card-name"><?php echo esc_html($name); ?></span><span class="card-arrow">→</span></span>
<span class="card-tag"><?php echo esc_html($tag); ?></span>
<span class="card-short"><?php echo esc_html($short); ?></span>
</span>
</a>
<?php
}
/* ── Noindex partout sauf l'accueil (demande du 06/10/2026) ──────────
Seule la page d'accueil reste indexable ; pages, réalisations, articles,
archives, recherche, 404… sont en noindex. */
add_filter('wp_robots', function ($robots) {
if (is_front_page()) {
return $robots;
}
unset($robots['max-image-preview']);
$robots['noindex'] = true;
$robots['nofollow'] = true;
return $robots;
}, 99);
/* Même consigne pour les réponses non-HTML générées par WordPress (flux RSS, REST…) */
add_action('send_headers', function () {
if (!is_admin() && !is_front_page()) {
header('X-Robots-Tag: noindex, nofollow', true);
}
});
/* ── Anti-doublon : retire du contenu l'image déjà en mise-en-avant ──── */
add_filter('the_content', function ($content) {
if (!is_singular('realisation') || !has_post_thumbnail()) {
return $content;
}
$thumb_url = wp_get_attachment_image_url(get_post_thumbnail_id(), 'full');
if (!$thumb_url) return $content;
$basename = pathinfo(parse_url($thumb_url, PHP_URL_PATH), PATHINFO_FILENAME);
$basename = preg_quote($basename, '#');
$content = preg_replace('#<figure\b[^>]*>(?:(?!</figure>).)*?' . $basename . '(?:(?!</figure>).)*?</figure>#is', '', $content);
$content = preg_replace('#<img\b[^>]*?' . $basename . '[^>]*?/?>#i', '', $content);
$content = preg_replace('#<p>\s*</p>#i', '', $content);
return $content;
}, 20);
/* ── Options pages ACF (shim) ────────────────────────────────────────── */
add_action('acf/init', function () {
if (function_exists('acf_add_options_page')) {
acf_add_options_page(array(
'page_title' => 'Theme General Settings',
'menu_title' => 'Theme Settings',
'menu_slug' => 'theme-general-settings',
'capability' => 'edit_posts',
'redirect' => false,
));
acf_add_options_sub_page(array('page_title' => 'Theme Header Settings', 'menu_title' => 'Header', 'parent_slug' => 'theme-general-settings'));
acf_add_options_sub_page(array('page_title' => 'Theme Social Settings', 'menu_title' => 'Social', 'parent_slug' => 'theme-general-settings'));
acf_add_options_sub_page(array('page_title' => 'Theme Coordonnée Settings', 'menu_title' => 'Coordonnée', 'parent_slug' => 'theme-general-settings'));
}
});
/* Autoptimize : ne pas agréger le script inline anti-flash du wipe
(il doit s'exécuter immédiatement après l'élément #yb-wipe, avant le paint). */
add_filter('autoptimize_filter_js_exclude', function ($exclude) {
return $exclude . ', ybWipe, ybTheme';
});
/* Shortcode hérité (présent dans d'anciens contenus) */
add_shortcode('tavu', function () {
return "<h1>T'a vue ?</h1>";
});
/* Ancienne adresse de la page Compétences : /services/ → /competences/ (301).
Les contenus du CPT « service » gardent leurs adresses /services/<slug>/. */
add_action('template_redirect', function () {
$path = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
if ($path === 'services') {
wp_redirect(home_url('/competences/'), 301);
exit;
}
}, 1);
/* ── Ressources tierces : rien n'est chargé depuis un CDN externe côté visiteur ── */
add_action('wp_enqueue_scripts', function () {
wp_dequeue_style('acf-shim-fontawesome'); // icônes Font Awesome de l'admin ACF, inutiles sur le site
}, 100);