JSON-LD malformato: come trovarlo e fixarlo prima che Google lo ignori
Una virgola di troppo alla fine di un array e Google salta l'intero blocco di schema markup. Nessun errore visibile, nessun warning in console. Solo rich results che non appaiono mai.
Durante l'audit di smartweb-media.com, uno script Python ha trovato un JSON-LD non valido su blog.html. Il sito lo serviva da settimane. Google lo ignorava silenziosamente da altrettante settimane.
Il problema: una trailing comma alla fine dell'ultimo elemento nell'array blogPost. In JavaScript questo è spesso accettato dai parser moderni. In JSON puro — che è lo standard che Google usa — è un errore sintattico fatale.
Ti mostro il problema, come trovarlo su tutto il sito automaticamente, e come fixarlo in modo che non si ripeta.
Il caso reale: trailing comma su blog.html
Ecco il JSON-LD problematico trovato su blog.html:
❌ JSON-LD malformato — blog.html
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Blog",
"name": "Smart SEO Media Blog",
"blogPost": [
{
"@type": "BlogPosting",
"headline": "Perché il tuo traffico non converte?",
"url": "https://smartweb-media.com/blog/traffico-non-converte.html",
"datePublished": "2024-01-15"
},
{
"@type": "BlogPosting",
"headline": "SEO nel 2026: L'AI ha trasformato la ricerca?",
"url": "https://smartweb-media.com/blog/seo-ai-2024.html",
"datePublished": "2024-02-08"
}, ← ERRORE: virgola dopo l'ultimo elemento
] ← JSON standard non ammette trailing comma
}
</script>
✅ JSON-LD corretto — dopo il fix
{
"@type": "BlogPosting",
"headline": "SEO nel 2026: L'AI ha trasformato la ricerca?",
"url": "https://smartweb-media.com/blog/seo-ai-2024.html",
"datePublished": "2024-02-08"
} ← nessuna virgola dopo l'ultimo elemento
]
}
La differenza è una singola virgola. L'impatto: Google non legge nessuno dei tuoi BlogPosting, nessun dato strutturato viene processato, nessun rich result viene generato.
Perché è difficile da accorgersi
Tre ragioni per cui questo errore rimane nascosto a lungo:
1. I browser non la segnalano
I browser moderni (Chrome, Firefox) hanno parser JSON lenient che accettano trailing comma. Aprire la pagina, ispezionare i tag script — nessun errore visibile nella console del browser.
2. La Search Console non invia alert immediati
Google Search Console segnala i problemi di schema solo nella sezione "Miglioramenti" e solo per tipo di schema. Se non ci sono mai stati rich results per quella pagina, non c'è nulla da confrontare — e nessun alert viene generato.
3. L'errore non rompe la pagina
La pagina funziona perfettamente. Gli utenti non vedono nulla di strano. Solo i crawler — Google, Bing, i parser di LinkedIn e Twitter — ricevono dati strutturati non validi e li scartano.
Script Python: audit JSON-LD su tutto il sito
Invece di controllare pagina per pagina nel Rich Results Test, ho scritto uno script che legge la sitemap, scarica ogni pagina e valida tutti i blocchi JSON-LD in uno shot:
audit_json_ld.py
import requests
import re
import json
import xml.etree.ElementTree as ET
from urllib.parse import urljoin
def fetch_page(url, timeout=10):
"""Scarica HTML di una pagina."""
try:
resp = requests.get(url, timeout=timeout,
headers={'User-Agent': 'Mozilla/5.0'})
return resp.text if resp.status_code == 200 else None
except Exception:
return None
def extract_json_ld_blocks(html):
"""Estrae tutti i blocchi JSON-LD dall'HTML."""
pattern = r''
return re.findall(pattern, html, re.DOTALL | re.IGNORECASE)
def validate_json_ld(block):
"""Valida un singolo blocco JSON-LD."""
try:
data = json.loads(block.strip())
schema_type = data.get('@type', 'Unknown') if isinstance(data, dict) else 'Array'
return {'valid': True, 'type': schema_type, 'error': None}
except json.JSONDecodeError as e:
return {'valid': False, 'type': None, 'error': str(e)}
def find_common_errors(block):
"""Identifica errori comuni nel JSON-LD."""
errors = []
# Trailing comma prima di ] o }
if re.search(r',\s*[\]\}]', block):
errors.append('trailing_comma')
# Apici singoli invece di doppi
if re.search(r"'[^']+'\s*:", block):
errors.append('single_quotes')
# Proprietà senza virgolette
if re.search(r'^\s*\w+\s*:', block, re.MULTILINE):
errors.append('unquoted_keys')
return errors
def audit_site_json_ld(sitemap_url):
"""Audita JSON-LD su tutto il sito dalla sitemap."""
# Scarica sitemap
resp = requests.get(sitemap_url, timeout=10)
root = ET.fromstring(resp.content)
ns = {'sm': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
urls = [loc.text for loc in root.findall('.//sm:loc', ns)]
print(f"Audit JSON-LD su {len(urls)} pagine\n{'='*60}")
results = {'ok': [], 'errors': [], 'no_schema': []}
for url in urls:
html = fetch_page(url)
if not html:
print(f" ⚠️ Impossibile scaricare: {url}")
continue
blocks = extract_json_ld_blocks(html)
path = url.split('.com')[-1] or '/'
if not blocks:
results['no_schema'].append(url)
print(f" ○ {path:<50} nessun JSON-LD")
continue
page_ok = True
for i, block in enumerate(blocks):
validation = validate_json_ld(block)
if validation['valid']:
print(f" ✅ {path:<50} schema #{i+1}: {validation['type']}")
results['ok'].append({'url': url, 'type': validation['type']})
else:
common = find_common_errors(block)
error_type = common[0] if common else 'syntax_error'
print(f" ❌ {path:<50} schema #{i+1}: {validation['error']}")
print(f" Tipo errore probabile: {error_type}")
results['errors'].append({
'url': url,
'block_index': i + 1,
'error': validation['error'],
'error_type': error_type,
'block_preview': block.strip()[:200]
})
page_ok = False
print(f"\n{'='*60}")
print(f"RIEPILOGO:")
print(f" ✅ Schemi validi: {len(results['ok'])}")
print(f" ❌ Errori trovati: {len(results['errors'])}")
print(f" ○ Senza schema: {len(results['no_schema'])}")
if results['errors']:
print(f"\nPAGINE DA FIXARE:")
for err in results['errors']:
print(f" {err['url']}")
print(f" → {err['error_type']}: {err['error']}")
return results
# Fix automatico per trailing comma
def fix_trailing_commas(html):
"""Rimuove trailing comma da tutti i blocchi JSON-LD nell'HTML."""
def fix_block(match):
block = match.group(1)
# Rimuovi trailing comma prima di ] o }
fixed = re.sub(r',(\s*[\]\}])', r'\1', block)
return match.group(0).replace(block, fixed)
pattern = r'()'
return re.sub(pattern, fix_block, html, flags=re.DOTALL | re.IGNORECASE)
# Uso
if __name__ == '__main__':
results = audit_site_json_ld('https://smartweb-media.com/sitemap.xml')
Gli errori JSON-LD più comuni
1. Trailing comma (il più frequente)
❌ "name": "Articolo 3",
]
✅ "name": "Articolo 3"
]
2. Apici singoli invece di doppi
❌ '@type': 'BlogPosting'
✅ "@type": "BlogPosting"
3. URL senza HTTPS
❌ "url": "http://tuosito.com/pagina"
✅ "url": "https://tuosito.com/pagina"
4. datePublished in formato non ISO 8601
❌ "datePublished": "30/03/2026"
✅ "datePublished": "2026-03-30"
5. @context mancante o sbagliato
❌ "@context": "http://schema.org"
✅ "@context": "https://schema.org"
Valida manualmente con il Rich Results Test
Per una verifica rapida senza script:
- Vai su search.google.com/test/rich-results
- Inserisci l'URL della pagina da verificare
- Google analizza i dati strutturati e mostra errori e warning
- Per ogni errore, clicca sul dettaglio per vedere il campo problematico
Lo svantaggio: devi farlo pagina per pagina. Per siti con 20+ pagine con schema markup, lo script Python è più efficiente.
Prevenire: JSON-LD generato programmaticamente
Il modo più sicuro per evitare errori JSON-LD è generarli programmaticamente invece di scriverli a mano nell'HTML:
genera_schema.py
import json
def generate_blog_schema(posts):
"""Genera JSON-LD Blog valido da una lista di post."""
schema = {
"@context": "https://schema.org",
"@type": "Blog",
"name": "Smart SEO Media Blog",
"description": "Knowledge Base SEO: strategie e guide operative",
"url": "https://smartweb-media.com/blog.html",
"publisher": {
"@type": "Organization",
"name": "Smart SEO Media"
},
"blogPost": [
{
"@type": "BlogPosting",
"headline": post['title'],
"url": post['url'],
"datePublished": post['date'],
"author": {"@type": "Person", "name": "Antonio Montingelli"}
}
for post in posts
]
}
# json.dumps garantisce JSON valido — niente trailing comma mai
return json.dumps(schema, ensure_ascii=False, indent=2)
# Uso
posts = [
{"title": "Come uso Claude per fare audit SEO", "url": "https://smartweb-media.com/blog/seo-audit-con-claude-pipeline.html", "date": "2026-03-02"},
{"title": "Search Console + Python: quick wins in 10 minuti", "url": "https://smartweb-media.com/blog/search-console-quick-wins.html", "date": "2026-03-30"}
]
schema_json = generate_blog_schema(posts)
print(schema_json) # JSON sempre valido, zero rischio trailing comma
Hai schema markup sul tuo sito? Controlliamolo.
Giro lo script di audit JSON-LD su tutto il sito, trovo errori e warning, e ti consegno un report con i fix prioritizzati. Nessun cost aggiuntivo nell'audit gratuito.
Richiedi Audit Gratuito