Oxygen Builder e Woocommerce: come fixare la Product List dopo l’ultimo aggiornamento

Product list non visibile con Oxygen Builder e Woocommerce

Chi lavora con Oxygen Builder e WooCommerce lo sa: ogni tanto qualche aggiornamento importante di WooCommerce manda in tilt cose che prima funzionavano senza battere ciglio. Siamo compagni di viaggio su questo sentiero lastricato di frustrazione, specie se gestisci decine di siti e devi trovare una soluzione veloce … o scappare su un’isola dove non ci sia internet e copertura telefonica.

È successo di recente con la Product List: improvvisamente le gallerie e gli script legati ai prodotti singoli hanno smesso di funzionare. E con ciò intendo dire che non si vedono più, di punto in bianco e senza motivazioni visibili, che permettessero un minimo di retroingegneria.

Risultato? Un front-end spezzato, con utenti che non vedono più zoom, lightbox o variazioni, clienti felicissimi di ciò e ore spese in ricerca della soluzione.

Dopo un poco di tempo che ero andato avanti forzando l’opacità delle immagini in product list, mi sono reso conto che il problema era lato Javascript, non perché avessi degli utili errori che mi mettessero sulla via della risoluzione, ma perché anche i tab, il lightbox e quant’altro non funzionavano nemmeno per il … caso.

Il CSS era questo:

.woocommerce-product-gallery {
    opacity: 1 !important;
}

Funzionicchiava, certo i prodotti si vedevano, ma niente lightbox, niente tab prodotto avrebbero certamente degradato l’esperienza utente.

Allora, ci siamo

Gira che gira, tienimi che ti tengo, abbiamo trovato la discussione utile in questo gruppo Facebook: Oxygen Builder Community – Fix post, grazie Henio Henio <3.

A quanto si è capito che WooCommerce non carica più automaticamente lo script wc-single-product nella Product List, o forse sarebbe meglio dire, che Woocommerce ha cambiato le carte in tavola e ha sfondato il modo in cui gli script venissero caricati per il prodotto singolo in Oxygen Builder.

Morale della favola: c’era bisogno di forzarlo.

La soluzione rapida: code block PHP

Se vuoi un fix immediato, puoi aprire il tuo template con Oxygen e inserire un Code Block PHP con dentro:

&lt;?php
  wp_enqueue_script('wc-single-product');
?>

Dove metterlo? Nel template del singolo prodotto ovviamente.

Questo dice a WordPress: “Ehi, quando sei in una pagina prodotto, carica anche lo script che serve a far girare la galleria, lo zoom e tutte le funzioni fighe del single product”.

Ha funzionato benissimo.

La soluzione elegante: un MU-Plugin

NOTA: questa a me non ha funzionato e sono virato sul code block.

Sei un maniaco come me e non vuoi insozzare il sorgente?

Se vuoi una soluzione permanente e meno “pezza a colori”, puoi creare un mu-plugin. Basta aprire la cartella wp-content/mu-plugins/ (se non esiste, creala) e inserire un file chiamato fix-oxygen-wc-single-product.php con questo codice:

&lt;?php
/**
 * Plugin Name: Fix Oxygen WooCommerce Single Product Scripts
 * Description: Forza il caricamento dello script wc-single-product nelle pagine prodotto WooCommerce.
 * Author: Un Moscerino nel Web
 * Version: 1.0
 */

add_action('wp_enqueue_scripts', function() {
    if (is_product()) {
        wp_enqueue_script('wc-single-product');
    }
}, 20);

In questo modo non dipendi da Oxygen né da aggiornamenti strani: lo script viene caricato solo quando serve, senza sporcare i tuoi template.

Ripeto, questo modus dovrebbe essere il migliore in teoria, ma a me non ha funzionato.

Tirando le cu… somme

Il bello di lavorare con WordPress è che riesci a tirare su un sito in poco tempo e ci sono migliaia e migliaia di soluzioni già pronte. Il brutto ? Lavorare con WordPress + Oxygen + WooCommerce è che gli aggiornamenti non perdonano. A volte tocca rimboccarsi le maniche e aggiungere due righe di codice per rimettere tutto in sesto.
A volte bisogna fare dei rollback, ripristini, rollback di vecchie versioni eccetera.

Sono questi i momenti in cui commisero me stesso per non aver creato la mia soluzione con Laravel o chissà cosa per essere indipendente dal mondo …

Laravel e le traduzioni: tra JSON, file PHP e cache da ripulire

Laravel Translations

Se usi Laravel per progetti un po’ più complessi del classico “todo list”, prima o poi ti scontri con il sistema di traduzioni. Funziona bene, è flessibile, ma ha le sue particolarità — e se non sai dove mettere le mani rischi di impazzire davanti a un “These credentials do not match our records” che continua a spuntare in inglese anche se hai già scritto la tua bella frase in italiano.

Vediamo come funziona davvero, senza fumo negli occhi.

Due strade: JSON e file PHP

Laravel supporta due approcci per le traduzioni:

1. File JSON (resources/lang/it.json)

  • È un file unico per lingua.
  • La chiave è la stringa letterale in inglese, il valore è la traduzione.
  • Esempio: { "Log in": "Accedi", "Remember me": "Ricordami" }
  • Usato soprattutto dalle viste generate da Breeze, Jetstream e compagnia bella, che scrivono direttamente __('Log in') o __('Remember me').

È semplice e veloce: traduci stringa per stringa senza namespace.

2. File PHP (resources/lang/it/auth.php, validation.php, ecc.)

  • Qui le chiavi sono logiche, non testuali.
  • Esempio per l’autenticazione: <?php return [ 'failed' => 'Le credenziali non corrispondono ai nostri dati.', 'password' => 'La password fornita non è corretta.', ];
  • In questo caso il codice chiama __('auth.failed') invece della stringa intera.

È il sistema usato da Laravel “interno”: errori di validazione, messaggi di login, reset password, ecc.

Locale e fallback

Le traduzioni vengono lette in base al locale impostato.

  • In config/app.php: 'locale' => 'it', 'fallback_locale' => 'en',
  • Oppure nel .env: APP_LOCALE=it

Attenzione: se hai fatto php artisan config:cache, il .env non viene più letto direttamente, quindi ti conviene hardcodare locale in config/app.php o ripulire la cache.

La cache, maledetta cache

Se traduzioni e locale non cambiano, il 90% delle volte è colpa della cache.
Ripulire è la prima cosa da fare:

php artisan optimize:clear
php artisan config:clear
php artisan cache:clear
php artisan view:clear

Dopo, ricarica la pagina con un hard refresh del browser.
Se vuoi essere ancora più sicuro, testa in Tinker:

php artisan tinker
>>> app()->getLocale()
"it"
>>> __('auth.failed')
"Le credenziali non corrispondono ai nostri dati."

Se qui funziona, funziona anche nel browser.

JSON o PHP? Quando usare cosa

  • JSON → per le label e i testi “visibili” nelle view (Log in, Forgot your password?).
  • PHP → per i messaggi di sistema (auth.failed, errori di validazione, reset password).

Puoi usarli anche insieme senza problemi: Laravel li fonde.

Personalizzare i messaggi di sistema

Vuoi cambiare il testo dell’errore di login? Vai in resources/lang/it/auth.php:

'failed' => 'Utente o password non validi.',

Vuoi tradurre la frase lunga del “Forgot your password?”?
Mettila nel JSON:

{
  "Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.": "Hai dimenticato la password? Nessun problema: inserisci la tua email e ti invieremo un link per reimpostarla."
}

Debugging: checklist rapida

  1. Locale impostato in config/app.php
  2. Cache ripulita (optimize:clear, config:clear, ecc.)
  3. Stringa giusta nel file giusto (auth.failed nei PHP, testi letterali nel JSON)
  4. Test con Tinker: __('auth.failed') deve restituire italiano

Conclusione

Il sistema di traduzione di Laravel è flessibile, ma non sempre intuitivo se non sai la differenza tra file JSON e file PHP namespaced.
La regola è semplice: UI nel JSON, messaggi di sistema nei PHP. E quando non funziona… la colpa è quasi sempre della cache.

WireGuard su Ubuntu 22: icona tray con Python (stato + start/stop)

Quando ho dovuto scegliere come collegarmi in VPN ad un servizio per faccende di lavoro, mi sono trovato con il mio pinguino a dover scegliere come farlo, dopo qualche prova e qualche esperimento le mie scelte sono converg… convers… finite su Wireguard, che sostanzialmente era quello che rompeva meno le scatole.

HINT!

Chiunque usi una VPN con WireGuard sa che il comando magico è sempre quello:

sudo systemctl start wg-quick@wg0

Hai bisogno di configurare WireGuard?

Pratico, sì… ma un po’ spartano: niente icona, niente stato visibile, niente click per avviare o fermare, solo terminale bruto … bene ma non benissimo insomma.

Sul mio bell’Ubuntu 22 ho spazio a sufficienza in alto a destra, per mostrare una meravigliosa pallina verde o rossa (no, non è la contrada dell’Oca) che ti dica qualcosa sullo stato del servizio.
Con un poco di intelligenza naturale e un poco di quella artificiale ho trovato una soluzione elegante.

L’idea è avere

  • Una piccola icona nella tray bar (AppIndicator di GNOME).
  • Pallina verde/grigia/rossa a seconda dello stato (active, inactive, failed).
  • Menu a tendina con le voci: Start, Stop, Restart, Quit.
  • Una riga in fondo al menu: Current status: Active/Stopped/Error.
  • Notifica desktop all’avvio e ad ogni cambio di stato (così se succede qualcosa, casca il mondo o la connessione, almeno ti si avvisa).

Ci spiego (credetemi lo sto facendo soprattutto per me stesso futuro), come raggiungere il risultato from scratch, come si dice in quella lingua tonta che è l’inglese.

Requisiti

Prima installiamo i pacchetti necessari:

sudo apt update
sudo apt install -y python3-gi gir1.2-appindicator3-0.1 python3-gi-cairo gir1.2-notify-0.7

Per comodità, conviene permettere all’utente corrente di gestire il servizio WireGuard senza dover digitare ogni volta la password.
Nota che, non è una cosa obbligatoria, anche se in alcuni contesti può essere utile per motivi di sicurezza.
Nel mio caso era utile non stare là a mettere password, così ho aggiunto questa regola ai sudoers:

echo "$USER ALL=NOPASSWD: /bin/systemctl start wg-quick@wg0, /bin/systemctl stop wg-quick@wg0, /bin/systemctl restart wg-quick@wg0" \
| sudo tee /etc/sudoers.d/wg-indicator

Controlliamo che sia tutto valido:

sudo visudo -cf /etc/sudoers.d/wg-indicator

Qui dovrebbe venir fuori qualcosa tipo “/etc/sudoers.d/wg-indicator: analisi effettuata correttamente”

Lo script in Python

Ed ecco il cuore della faccenda: ~/.local/bin/wg-indicator.py. Creati un attimo questo file (per esempio con touch o nano o come ti pare).

Lo script usa GTK, AppIndicator e Notify. Crea tre icone PNG per le palline colorate, gestisce i comandi systemctl e aggiorna lo stato ogni 3 secondi.

#!/usr/bin/env python3
import os, subprocess, pathlib
import gi
gi.require_version('AppIndicator3', '0.1')
gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')
from gi.repository import AppIndicator3, Gtk, GLib, Notify, GdkPixbuf

SERVICE = "wg-quick@wg0"
APP_ID  = "wg-indicator"
ICON_DIR = os.path.join(pathlib.Path.home(), ".local", "share", "wg-indicator")
ICONS = {
    "green": os.path.join(ICON_DIR, "dot-green.png"),
    "grey":  os.path.join(ICON_DIR, "dot-grey.png"),
    "red":   os.path.join(ICON_DIR, "dot-red.png"),
}

# stato precedente per notifiche su cambio stato
last_state = None

def ensure_icons():
    os.makedirs(ICON_DIR, exist_ok=True)
    try:
        from PIL import Image, ImageDraw  # se Pillow c'è, icone più “tonde”
        for name, color in (("green",(0,185,0)), ("grey",(140,140,140)), ("red",(220,0,0))):
            path = ICONS[name]
            if not os.path.exists(path):
                img = Image.new("RGBA", (24,24), (0,0,0,0))
                d = ImageDraw.Draw(img)
                d.ellipse((3,3,21,21), fill=color+(255,), outline=(0,0,0,40))
                img.save(path, "PNG")
    except Exception:
        # fallback senza Pillow
        for name, rgba in (("green",(0,185,0,255)), ("grey",(140,140,140,255)), ("red",(220,0,0,255))):
            path = ICONS[name]
            if os.path.exists(path): 
                continue
            pix = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 24, 24)
            pix.fill(0x00000000)
            rs, gs, bs, a = rgba
            rowstride = pix.get_rowstride()
            pixels = pix.get_pixels()
            for y in range(24):
                for x in range(24):
                    dx, dy = x-12, y-12
                    if dx*dx + dy*dy &lt;= 9*9:
                        off = y*rowstride + x*4
                        pixels[off+0] = rs
                        pixels[off+1] = gs
                        pixels[off+2] = bs
                        pixels[off+3] = a
            pix.savev(path, "png", [], [])

def svc_status():
    try:
        out = subprocess.check_output(["systemctl","is-active",SERVICE], text=True).strip()
    except subprocess.CalledProcessError:
        out = "inactive"
    if out in ("active","inactive","activating","deactivating","reloading"):
        return out
    try:
        f = subprocess.check_output(["systemctl","is-failed",SERVICE], text=True).strip()
        if f == "failed":
            return "failed"
    except subprocess.CalledProcessError:
        pass
    return "unknown"

def human_status(state):
    return {
        "active":"Active",
        "inactive":"Stopped",
        "activating":"Starting…",
        "deactivating":"Stopping…",
        "reloading":"Reloading…",
        "failed":"Error",
        "unknown":"Error",
    }.get(state, "Error")

def color_for_status(state):
    if state == "active": return "green"
    if state in ("inactive","activating","deactivating","reloading"): return "grey"
    return "red"  # failed/unknown

def icon_for_status(state):
    return ICONS[color_for_status(state)]

def notify_status(prefix=None, state=None):
    if state is None:
        state = svc_status()
    text = human_status(state)
    title = "WireGuard Indicator"
    body = f"{('' if not prefix else prefix + ' ')}Current status: {text}"
    n = Notify.Notification.new(title, body, None)
    n.set_timeout(3000)
    try: n.show()
    except: pass

def update_status_label(state):
    # Aggiorna la label "Current status: X" con X colorato
    global status_label
    color = color_for_status(state)
    text = human_status(state)
    status_label.set_markup(f'&lt;span alpha="80%">Current status: &lt;/span>'
                            f'&lt;span weight="bold" foreground="{color}">{text}&lt;/span>')

def refresh(ind):
    global last_state
    state = svc_status()
    ind.set_icon_full(icon_for_status(state), f"WireGuard: {human_status(state)}")
    ind.set_label("", "")  # niente testo accanto all’icona
    update_status_label(state)
    if last_state is None:
        # notifica all’avvio (load)
        notify_status(prefix="", state=state)
    elif state != last_state:
        # notifica su ogni cambio stato (anche se avviene fuori dal menu)
        notify_status(prefix="", state=state)
    last_state = state
    return True

def run_cmd(cmd, verb):
    try:
        subprocess.check_call(["sudo","-n"] + cmd)
        # La notifica “di conferma” la facciamo dopo, quando rileviamo lo stato aggiornato
        # (così è affidabile). Però diamo un hint immediato:
        notify_status(prefix=f"{verb} requested.", state=None)
    except subprocess.CalledProcessError:
        # Chiede autorizzazione grafica
        ret = subprocess.call(["pkexec"] + cmd)
        if ret != 0:
            n = Notify.Notification.new("WireGuard Indicator", f"{verb} failed (authorization?)", None)
            n.set_timeout(3000)
            try: n.show()
            except: pass
    # Aggiorna subito l’icona; ulteriori cambi verranno col polling
    GLib.idle_add(refresh, indicator)

def on_start(_):   run_cmd(["/bin/systemctl","start",SERVICE], "Start")
def on_stop(_):    run_cmd(["/bin/systemctl","stop",SERVICE], "Stop")
def on_restart(_): run_cmd(["/bin/systemctl","restart",SERVICE], "Restart")

def on_quit(_):
    # Notifica finale con stato corrente e poi quit
    notify_status(prefix="", state=svc_status())
    Gtk.main_quit()

def build_menu():
    global status_label
    menu = Gtk.Menu()

    for label, cb in (("Start", on_start), ("Stop", on_stop), ("Restart", on_restart)):
        item = Gtk.MenuItem(label=label); item.connect("activate", cb); menu.append(item)

    menu.append(Gtk.SeparatorMenuItem())

    quit_item = Gtk.MenuItem(label="Quit")
    quit_item.connect("activate", on_quit)
    menu.append(quit_item)

    # Status non cliccabile, sotto Quit
    menu.append(Gtk.SeparatorMenuItem())
    status_item = Gtk.MenuItem()
    status_item.set_sensitive(False)  # non cliccabile
    status_label = Gtk.Label()
    status_label.set_use_markup(True)
    status_label.set_xalign(0.0)
    status_item.add(status_label)
    status_item.show_all()
    menu.append(status_item)

    menu.show_all()
    return menu

# ---- main
ensure_icons()
Notify.init(APP_ID)

indicator = AppIndicator3.Indicator.new(APP_ID, ICONS["grey"], AppIndicator3.IndicatorCategory.SYSTEM_SERVICES)
indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
indicator.set_menu(build_menu())

# primo refresh + polling
refresh(indicator)
GLib.timeout_add_seconds(3, lambda: refresh(indicator))
Gtk.main()

Salviamo, rendiamo eseguibile:

chmod +x ~/.local/bin/wg-indicator.py
~/.local/bin/wg-indicator.py

Magia: in alto a destra compare la pallina (si spera).

Avvio automatico

Per farlo partire ad ogni login, creiamo un file .desktop:

mkdir -p ~/.config/autostart
cat > ~/.config/autostart/wg-indicator.desktop &lt;&lt;'EOF'
[Desktop Entry]
Type=Application
Name=WireGuard Indicator
Exec=/usr/bin/env python3 /home/YOURUSER/.local/bin/wg-indicator.py
X-GNOME-Autostart-enabled=true
EOF

Ovviamente dove vedi YOURUSER ci va il nome utente tuo.

In conclusione direi che …

Questo piccolo script fa quello che GNOME non ci regala di default: un indicatore di stato semplice e comprensibile per WireGuard. È pensato per wg-quick@wg0, ma basta cambiare la variabile SERVICE se usate un’altra interfaccia.

Ci sono alternative, a quanto mi suggeriva il ciattone GPT, ma se siete fan del fai-da-te digitale, questo approccio con Python vi dà più flessibilità e zero dipendenze strane, cosa che talvolta è preferibile.

Spero ciò sia utile a qualcheduno che respira, non solo ai bot delle AI che stanno in giro a ciucciare contenuti.

🪰