What does Google say about SEO? /
Quick SEO Quiz

Test your SEO knowledge in 3 questions

Less than 30 seconds. Find out how much you really know about Google search.

🕒 ~30s 🎯 3 questions 📚 SEO Google

Official statement

Google doesn't really care about file extensions (.py, .txt, .php). What counts is the content-type header sent by the server and the actual content returned when accessing the URL.
🎥 Source video

Extracted from a Google Search Central video

💬 EN 📅 08/09/2022 ✂ 12 statements
Watch on YouTube →
Other statements from this video 11
  1. Google indexe-t-il vraiment vos PDF ou les transforme-t-il d'abord ?
  2. Le poids du contenu varie-t-il selon son emplacement en HTML et en PDF ?
  3. Google dépend-il vraiment d'Adobe pour indexer vos PDF ?
  4. Google indexe-t-il vraiment le code source comme du texte ordinaire ?
  5. Pourquoi les fichiers de code source peinent-ils à se classer dans Google ?
  6. Faut-il vraiment arrêter de stocker tous vos PDF dans un dossier /pdfs/ ?
  7. Pourquoi Google n'indexe-t-il jamais une image isolée sans page d'hébergement ?
  8. Google indexe-t-il vraiment les images et vidéos différemment du texte ?
  9. Google filtre-t-il les données personnelles avant indexation ?
  10. Google indexe-t-il vraiment tous vos fichiers XML ?
  11. Peut-on vraiment indexer des fichiers JSON et texte brut sans méta-données ?
📅
Official statement from (3 years ago)
TL;DR

Google completely ignores file extensions in your URLs. What actually matters: the content-type header sent by your server and the actual page content. A .py file can be indexed as an HTML page if your server declares it correctly.

What you need to understand

Why does file extension seem important when it actually isn't?

A file extension (`.html`, `.php`, `.txt`, `.pdf`) is a relic of historical web architecture. It once indicated the content type or server-side language being used. Many SEOs still believe these extensions influence indexation.

Let's be honest: Google doesn't read the extension to understand your page. The search engine relies exclusively on the HTTP content-type header sent by your server in response, and the actual content returned. If your server declares `text/html` for a `.py` file, Google treats it as HTML.

What exactly is the content-type header?

The content-type header is a line of HTTP header information sent by the server that tells the browser (and Googlebot) what type of content it's receiving. Examples: `text/html` for HTML, `application/pdf` for a PDF, `text/plain` for plain text.

This declaration is what guides Google, not the `.php` or `.html` in the URL. If your server sends a content-type that conflicts with the actual content, you create confusion.

What happens if the content-type is misconfigured?

If your server declares `text/plain` for an HTML page, Google will interpret the content as plain text. Your `` tags, `<meta>` tags, links — everything will be ignored or poorly indexed.</p><p>Conversely, a file with an unusual extension (`.aspx`, `.cfm`, `.py`) will be indexed perfectly if the content-type is `text/html` and the content matches. <strong>The problem is never the extension, always the server configuration.</strong></p><ul><li>Google reads the <strong>HTTP content-type header</strong>, not the file extension in the URL</li><li>The extension (`.html`, `.php`, `.txt`) has no direct impact on indexation</li><li>A wrong content-type can block indexation even if the extension looks correct</li><li>The actual content returned must match the declared content-type</li><li>This rule applies to all file types crawled by Google</li></ul></div> </div> <div class="section-card ai-section"> <h2>SEO Expert opinion</h2> <div class="content" data-field="ai_expert_opinion"><h3>Is this statement consistent with real-world observations?</h3><p>Yes, absolutely. For years now, we've been successfully indexing URLs with no extension (e.g., `/products/shoes`) or with exotic extensions. Modern framework-based sites (React, Vue, Next.js) often generate URLs without `.html`, and Google indexes them perfectly.</p><p>What sometimes goes wrong: misconfigured servers that send incorrect HTTP headers. A poorly configured Apache server might return `text/plain` by default for unknown files, breaking indexation in the process.</p><h3>Does this mean you should completely ignore extensions in your URLs?</h3><p>No, and that's where nuance matters. The extension has no direct impact on <strong>crawling and indexation</strong>, but it can indirectly influence <strong>user behavior</strong> and URL perception.</p><p>A URL ending in `.pdf` clearly signals you're downloading a document. A URL like `/article.txt` might confuse the user. For UX reasons and <strong>click-through rates in search results</strong>, clean URLs without extensions or with `.html` remain preferable. This is no longer technical SEO, it's usability.</p><h3>In what cases does this rule create problems?</h3><p>Be careful with <strong>servers serving multiple content types through the same URL</strong> depending on the user-agent. If your server returns HTML to Googlebot but JSON to an API bot, you create a gap between what Google indexes and what other systems see. <strong>[To be verified]</strong> in your logs to avoid inconsistencies.</p><p>Another pitfall: CDNs or reverse proxies that modify HTTP headers in cache. If your origin sends the correct content-type but Cloudflare or Fastly replaces it with a generic header, Google indexes the wrong format.</p><div class="alert"><strong>Server configuration alert:</strong> Never rely on the extension in your URLs to diagnose an indexation problem. Always check the actual HTTP headers sent by your server using a tool like curl or Chrome DevTools. This is the only reliable way to confirm what Google receives.</div></div> </div> <div class="section-card ai-section"> <h2>Practical impact and recommendations</h2> <div class="content" data-field="ai_practical_impact"><h3>What should you actually check on your site?</h3><p>First step: <strong>audit your HTTP headers</strong>. Use `curl -I https://yoursite.com/page` or DevTools (Network tab) to verify that each page type returns the correct content-type. An HTML page should return `text/html; charset=UTF-8`, a PDF should return `application/pdf`, etc.</p><p>Second step: verify that your static files (CSS, JS, images) also have the correct headers. CSS served as `text/plain` might not be executed properly by the browser, degrading Google's rendering.</p><h3>What errors should you absolutely avoid?</h3><p>Never configure your server to force a content-type based solely on the file extension visible in the URL. If you rewrite `/article` to `/article.php` internally, ensure the final HTTP header is `text/html`, not `application/x-httpd-php`.</p><p>Avoid setups where content-type varies by user-agent without valid reason. Google hates cloaking, even unintentional cloaking. If you serve different content to Googlebot and users, you risk a manual penalty.</p><h3>How do you ensure optimal server configuration?</h3><p>For Apache, check your `.htaccess` or `httpd.conf` file — the `AddType` directive must be consistent. For Nginx, control the `types {}` block in `nginx.conf`. For Node.js servers, Express or Fastify handle content-types automatically, but verify your custom middleware.</p><ul class="checklist"><li>Audit all content-type headers with curl or DevTools</li><li>Fix inconsistencies between extension and HTTP header</li><li>Test extension-less URLs to confirm they return the correct type</li><li>Verify that internal rewrites don't break headers</li><li>Monitor server logs to spot MIME type-related 500 errors</li><li>Document server configuration to prevent regressions during deployments</li></ul><div class="summary">File extension has no direct impact on Google indexation, but misconfigured content-type headers can completely block your SEO performance. Audit your HTTP headers, fix inconsistencies, and ensure your server correctly declares the type of each resource. These technical optimizations may seem simple in theory, but they often touch server configuration, cache rules, and internal rewrites — areas where a single mistake can have massive consequences. If your web infrastructure is complex or you're uncomfortable with Apache/Nginx configurations, hiring an experienced technical SEO agency will save you costly errors and guarantee quick, lasting compliance.</div></div> </div> <!-- FAQ --> <div class="section-card"> <h2>❓ Frequently Asked Questions</h2> <div class="faq-list"> <details class="faq-item"> <summary class="faq-question">Puis-je utiliser des URLs sans extension comme /produits/chaussures pour le SEO ?</summary> <div class="faq-answer">Oui, absolument. Google indexe parfaitement les URLs sans extension tant que le serveur envoie le bon content-type header (text/html pour une page web). C'est même devenu la norme pour les sites modernes.</div> </details> <details class="faq-item"> <summary class="faq-question">Mon site affiche des fichiers .php dans les URLs, dois-je les masquer ?</summary> <div class="faq-answer">Ce n'est pas une obligation technique pour l'indexation, mais pour l'UX et la propreté des URLs, oui. Utilisez un rewrite Apache/Nginx pour transformer /page.php en /page, tout en gardant le bon content-type header.</div> </details> <details class="faq-item"> <summary class="faq-question">Comment vérifier le content-type header envoyé par mon serveur ?</summary> <div class="faq-answer">Utilisez la commande curl -I https://votresite.com/page dans un terminal, ou l'onglet Network des DevTools Chrome/Firefox. Cherchez la ligne 'content-type' dans les headers de réponse HTTP.</div> </details> <details class="faq-item"> <summary class="faq-question">Un fichier .txt peut-il être indexé comme une page HTML ?</summary> <div class="faq-answer">Oui, si votre serveur envoie un content-type: text/html et que le contenu est du HTML valide. L'extension .txt n'empêche pas l'indexation, mais attention à la cohérence pour ne pas dérouter les utilisateurs.</div> </details> <details class="faq-item"> <summary class="faq-question">Que se passe-t-il si mon CDN modifie le content-type header ?</summary> <div class="faq-answer">Google indexera selon le header final reçu par Googlebot, pas celui de votre serveur origine. Vérifiez les règles de cache de votre CDN (Cloudflare, Fastify, AWS CloudFront) pour garantir que les headers critiques sont préservés.</div> </details> </div> </div> <!-- Tags SEO --> <div class="seo-tags-block"> <span class="seo-tags-label">🏷 Related Topics</span> <div class="seo-tags"> <a href="/en/?q=content-type" class="seo-tag-pill">content-type</a> <a href="/en/?q=headers+HTTP" class="seo-tag-pill">headers HTTP</a> <a href="/en/?q=indexation" class="seo-tag-pill">indexation</a> <a href="/en/?q=URLs" class="seo-tag-pill">URLs</a> <a href="/en/?q=configuration+serveur" class="seo-tag-pill">configuration serveur</a> <a href="/en/?q=crawl" class="seo-tag-pill">crawl</a> <a href="/en/?q=architecture+web" class="seo-tag-pill">architecture web</a> </div> </div> <!-- Categories --> <div class="declaration-tags"> <a href="/en/c/contenu" class="tag" style="background: #10b98120; color: #10b981;">Content</a> <a href="/en/c/ia-seo" class="tag" style="background: #a855f720; color: #a855f7;">AI & SEO</a> <a href="/en/c/nom-domaine" class="tag" style="background: #7c3aed20; color: #7c3aed;">Domain Name</a> <a href="/en/c/pdf-fichiers" class="tag" style="background: #78716c20; color: #78716c;">PDF & Files</a> </div> <!-- Syntheses thematiques liees --> <!-- Autres declarations de la meme video YouTube --> <div class="section-card yt-same-video"> <h2> 🎥 From the same video <span class="yt-same-count">11</span> </h2> <p class="yt-same-intro"> Other SEO insights extracted from this same Google Search Central video · published on 08/09/2022 </p> <div class="related-grid yt-same-grid"> <a href="/en/d/google-convertit-les-pdf-en-html-pour-l-indexation" class="related-item yt-same-item"> <div class="title">Google indexe-t-il vraiment vos PDF ou les transforme-t-il d'abord ?</div> <div class="meta"> </div> </a> <a href="/en/d/poids-uniforme-du-contenu-dans-les-pdf-vs-html" class="related-item yt-same-item"> <div class="title">Le poids du contenu varie-t-il selon son emplacement en HTML et en PDF ?</div> <div class="meta"> </div> </a> <a href="/en/d/google-utilise-adobe-pour-convertir-les-pdf" class="related-item yt-same-item"> <div class="title">Google dépend-il vraiment d'Adobe pour indexer vos PDF ?</div> <div class="meta"> </div> </a> <a href="/en/d/google-indexe-le-code-source-comme-du-texte" class="related-item yt-same-item"> <div class="title">Google indexe-t-il vraiment le code source comme du texte ordinaire ?</div> <div class="meta"> </div> </a> <a href="/en/d/les-fichiers-de-code-ont-du-mal-a-bien-se-classer" class="related-item yt-same-item"> <div class="title">Pourquoi les fichiers de code source peinent-ils à se classer dans Google ?</div> <div class="meta"> </div> </a> <a href="/en/d/meilleure-pratique-repartir-les-pdf-par-sujet-pas-par-type" class="related-item yt-same-item"> <div class="title">Faut-il vraiment arrêter de stocker tous vos PDF dans un dossier /pdfs/ ?</div> <div class="meta"> </div> </a> <a href="/en/d/google-images-necessite-une-page-d-hebergement" class="related-item yt-same-item"> <div class="title">Pourquoi Google n'indexe-t-il jamais une image isolée sans page d'hébergement ?</div> <div class="meta"> </div> </a> <a href="/en/d/images-et-videos-utilisent-un-indexeur-different" class="related-item yt-same-item"> <div class="title">Google indexe-t-il vraiment les images et vidéos différemment du texte ?</div> <div class="meta"> </div> </a> <a href="/en/d/google-n-analyse-pas-le-contenu-prive-dans-les-documents" class="related-item yt-same-item"> <div class="title">Google filtre-t-il les données personnelles avant indexation ?</div> <div class="meta"> </div> </a> <a href="/en/d/google-peut-indexer-certains-fichiers-xml-mais-pas-tous" class="related-item yt-same-item"> <div class="title">Google indexe-t-il vraiment tous vos fichiers XML ?</div> <div class="meta"> </div> </a> <a href="/en/d/fichiers-json-et-texte-indexables-avec-contexte-externe" class="related-item yt-same-item"> <div class="title">Peut-on vraiment indexer des fichiers JSON et texte brut sans méta-données ?</div> <div class="meta"> </div> </a> </div> <a href="https://www.youtube.com/watch?v=2gXO6qpcQXY" target="_blank" rel="noopener" class="yt-same-source-link"> 🎥 Watch the full video on YouTube → </a> </div> <!-- Declarations similaires --> <div class="section-card"> <h2>Related statements</h2> <div class="related-grid"> <a href="/en/d/seo-is-complex-multifaceted-and-resilient" class="related-item"> <div class="title">Can we really afford to do anything in SEO without facing consequences?</div> <div class="meta"> John Mueller · Apr 2026 · <span class="stars">★★</span> </div> </a> <a href="/en/d/anyone-who-claims-to-know-everything-about-seo-is-a-fraud" class="related-item"> <div class="title">Why can't anyone truly master SEO 100%?</div> <div class="meta"> John Mueller · Apr 2026 · <span class="stars">★★★</span> </div> </a> <a href="/en/d/new-robots-txt-data-collection-with-http-archive" class="related-item"> <div class="title">Why is Google suddenly sharing massive data on robots.txt usage?</div> <div class="meta"> Gary Illyes · Apr 2026 · <span class="stars">★★★</span> </div> </a> <a href="/en/d/the-importance-of-http-archive-for-seo" class="related-item"> <div class="title">Is Google finally revealing how it really analyzes your pages with HTTP Archive?</div> <div class="meta"> Gary Illyes · Apr 2026 · <span class="stars">★★★</span> </div> </a> <a href="/en/d/sql-queries-in-bigquery-for-seo-analysis" class="related-item"> <div class="title">Do you really need to master SQL and BigQuery for SEO in 2025?</div> <div class="meta"> Gary Illyes · Apr 2026 · <span class="stars">★★</span> </div> </a> <a href="/en/d/using-bigquery-to-analyze-websites" class="related-item"> <div class="title">Is BigQuery really essential for analyzing your SEO data at scale?</div> <div class="meta"> Martin Splitt · Apr 2026 · <span class="stars">★★★</span> </div> </a> </div> </div> <!-- Prev/Next --> <div class="prev-next"> <a href="/en/d/uniform-content-weight-in-pdfs-vs-html"> <div class="nav-label">« Previous</div> <div class="nav-title">Uniform content weight in PDFs vs HTML...</div> </a> <a href="/en/d/google-converts-pdfs-to-html-for-indexing" style="text-align: right;"> <div class="nav-label">Next »</div> <div class="nav-title">Google Converts PDFs to HTML for Indexing...</div> </a> </div> <div class="back-link"> <a href="/en/" class="btn">« Back to results</a> </div> </div> <style> .comments-section { max-width: 860px; margin: 40px auto; padding: 0 20px; } .comments-title { font-size: 1.3rem; font-weight: 700; color: #1e293b; margin: 0 0 20px; padding-bottom: 12px; border-bottom: 2px solid #f1f5f9; } .comments-count { font-weight: 400; font-size: .9rem; color: #94a3b8; } .comments-empty { color: #94a3b8; font-style: italic; margin-bottom: 24px; } .comments-list { margin-bottom: 28px; } .comment-item { padding: 16px 0; border-bottom: 1px solid #f1f5f9; } .comment-item:last-child { border-bottom: none; } .comment-pending { background: #fffbeb; border-left: 3px solid #f59e0b; padding-left: 14px; border-radius: 6px; margin: 4px 0; } .comment-pending-badge { display: inline-block; font-size: .7rem; font-weight: 600; color: #92400e; background: #fef3c7; padding: 1px 8px; border-radius: 10px; margin-left: 8px; } .comment-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; } .comment-author { font-weight: 600; font-size: .95rem; color: #1e293b; } .comment-date { font-size: .8rem; color: #94a3b8; } .comment-body { font-size: .93rem; line-height: 1.6; color: #475569; } .comment-form { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; position: relative; } .comment-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; } .comment-form-field { margin-bottom: 12px; } .comment-form-field label { display: block; font-size: .85rem; font-weight: 600; color: #475569; margin-bottom: 4px; } .comment-form-field input, .comment-form-field textarea { width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: .9rem; font-family: inherit; background: #fff; transition: border-color .2s; box-sizing: border-box; } .comment-form-field input:focus, .comment-form-field textarea:focus { outline: none; border-color: #1a73e8; box-shadow: 0 0 0 3px rgba(26, 115, 232, .1); } .comment-form-field textarea { resize: vertical; min-height: 100px; } .comment-form-charcount { font-size: .75rem; color: #94a3b8; text-align: right; margin-top: 4px; } .comment-form-footer { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } .comment-submit-btn { padding: 10px 24px; background: #1a73e8; color: #fff; border: none; border-radius: 8px; font-size: .9rem; font-weight: 600; cursor: pointer; transition: opacity .2s; } .comment-submit-btn:hover { opacity: .88; } .comment-submit-btn:disabled { opacity: .5; cursor: default; } .comment-form-notice { font-size: .8rem; color: #94a3b8; font-style: italic; } .comment-feedback { margin-top: 12px; padding: 10px 14px; border-radius: 8px; font-size: .9rem; font-weight: 500; } .comment-feedback-success { background: #d1fae5; color: #065f46; } .comment-feedback-error { background: #fee2e2; color: #991b1b; } @media (max-width: 768px) { .comment-form-row { grid-template-columns: 1fr; gap: 0; } .comments-section { padding: 0 16px; } } </style> <section class="comments-section" id="comments"> <h3 class="comments-title">💬 Comments <span class="comments-count">(0)</span></h3> <div class="comments-list" id="comments-list"> <p class="comments-empty" id="comments-empty">Be the first to comment.</p> </div> <form class="comment-form" id="comment-form" novalidate> <input type="hidden" name="page_type" value="declaration"> <input type="hidden" name="page_id" value="3045"> <input type="hidden" name="lang" value="en"> <!-- Honeypot anti-spam --> <div style="position:absolute;left:-9999px;" aria-hidden="true"> <label for="website">Do not fill this field</label> <input type="text" id="website" name="website" tabindex="-1" autocomplete="off"> </div> <div class="comment-form-row"> <div class="comment-form-field"> <label for="comment-name">Name or alias *</label> <input type="text" id="comment-name" name="author_name" required maxlength="100" autocomplete="name"> </div> <div class="comment-form-field"> <label for="comment-email">Email (optional, not published)</label> <input type="email" id="comment-email" name="author_email" maxlength="255" autocomplete="email"> </div> </div> <div class="comment-form-field"> <label for="comment-content">Your comment *</label> <textarea id="comment-content" name="content" required maxlength="2000" rows="4"></textarea> <div class="comment-form-charcount"><span id="comment-chars">2000</span> characters remaining</div> </div> <div class="comment-form-footer"> <button type="submit" class="comment-submit-btn">Post comment</button> <span class="comment-form-notice">Comments are moderated before publication.</span> </div> <div id="comment-feedback" class="comment-feedback" style="display:none;"></div> </form> </section> <script> (function() { var form = document.getElementById('comment-form'); if (!form) return; var textarea = document.getElementById('comment-content'); var charsEl = document.getElementById('comment-chars'); var feedback = document.getElementById('comment-feedback'); var API_URL = 'https://seoclaims.com/api/comments.php'; // Compteur de caracteres textarea.addEventListener('input', function() { var remaining = 2000 - this.value.length; charsEl.textContent = remaining; charsEl.style.color = remaining < 100 ? '#dc2626' : ''; }); form.addEventListener('submit', function(e) { e.preventDefault(); var btn = form.querySelector('.comment-submit-btn'); btn.disabled = true; var origText = btn.textContent; btn.textContent = '...'; feedback.style.display = 'none'; var data = { action: 'submit', page_type: form.querySelector('[name="page_type"]').value, page_id: parseInt(form.querySelector('[name="page_id"]').value), author_name: form.querySelector('[name="author_name"]').value.trim(), author_email: form.querySelector('[name="author_email"]').value.trim(), content: form.querySelector('[name="content"]').value.trim(), lang: form.querySelector('[name="lang"]').value, website: form.querySelector('[name="website"]').value }; fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.success) { feedback.className = 'comment-feedback comment-feedback-success'; feedback.textContent = res.message; feedback.style.display = 'block'; // Inserer le commentaire en ligne (en attente de moderation) var list = document.getElementById('comments-list'); var empty = document.getElementById('comments-empty'); if (empty) empty.remove(); var pendingLabel = 'Awaiting moderation'; var now = new Date(); var day = String(now.getDate()).padStart(2,'0'); var month = String(now.getMonth()+1).padStart(2,'0'); var dateStr = day + '/' + month + '/' + now.getFullYear(); var div = document.createElement('div'); div.className = 'comment-item comment-pending'; div.innerHTML = '<div class="comment-meta">' + '<span class="comment-author">' + data.author_name.replace(/</g,'<') + '</span>' + '<span class="comment-date">' + dateStr + '</span>' + '<span class="comment-pending-badge">' + pendingLabel + '</span>' + '</div>' + '<div class="comment-body">' + data.content.replace(/</g,'<').replace(/\n/g,'<br>') + '</div>'; list.appendChild(div); form.querySelector('[name="content"]').value = ''; form.querySelector('[name="author_name"]').value = ''; form.querySelector('[name="author_email"]').value = ''; charsEl.textContent = '2000'; } else { feedback.className = 'comment-feedback comment-feedback-error'; feedback.textContent = res.error; feedback.style.display = 'block'; btn.disabled = false; btn.textContent = origText; } }) .catch(function() { feedback.className = 'comment-feedback comment-feedback-error'; feedback.textContent = 'Erreur réseau'; feedback.style.display = 'block'; btn.disabled = false; btn.textContent = origText; }); }); })(); </script> <section class="nl-inline-section"> <div class="nl-inline-inner"> <div class="nl-inline-icon">🔔</div> <div class="nl-inline-text"> <h2 class="nl-inline-headline">Get real-time analysis of the latest Google SEO declarations</h2> <p class="nl-inline-sub">Be the first to know every time a new official Google statement drops — with full expert analysis.</p> </div> <div class="nl-inline-action"> <form class="nl-inline-form" id="nl-inline-form" novalidate> <input type="email" name="email" placeholder="Your email address" required autocomplete="email"> <button type="submit">Subscribe for free</button> </form> <div class="nl-inline-privacy">No spam. Unsubscribe in one click.</div> </div> </div> </section> <style> .nl-inline-section { background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #0f172a 100%); padding: 52px 24px; margin: 0; } .nl-inline-inner { max-width: 960px; margin: 0 auto; display: grid; grid-template-columns: auto 1fr auto; gap: 24px 32px; align-items: center; } .nl-inline-icon { font-size: 40px; line-height: 1; } .nl-inline-headline { font-size: clamp(16px, 2vw, 20px); font-weight: 700; color: #f0f9ff; margin: 0 0 6px; line-height: 1.35; } .nl-inline-sub { font-size: 14px; color: #7fb8d8; margin: 0; line-height: 1.55; } .nl-inline-action { min-width: 300px; } .nl-inline-form { display: flex; gap: 8px; } .nl-inline-form input[type="email"] { flex: 1; padding: 11px 14px; border: 1px solid rgba(14,165,233,.4); border-radius: 8px; background: rgba(255,255,255,.07); color: #f0f9ff; font-size: 14px; outline: none; min-width: 0; transition: border-color .2s, background .2s; } .nl-inline-form input[type="email"]::placeholder { color: #5a91b0; } .nl-inline-form input[type="email"]:focus { border-color: #0ea5e9; background: rgba(255,255,255,.12); } .nl-inline-form button { padding: 11px 18px; background: linear-gradient(135deg, #0ea5e9, #7c3aed); color: #fff; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: opacity .2s; } .nl-inline-form button:hover { opacity: .88; } .nl-inline-form button:disabled { opacity: .55; cursor: default; } .nl-inline-privacy { font-size: 12px; color: #4a7a96; margin-top: 7px; text-align: center; } @media (max-width: 768px) { .nl-inline-inner { grid-template-columns: auto 1fr; grid-template-rows: auto auto; } .nl-inline-action { grid-column: 1 / -1; min-width: 0; } .nl-inline-form { flex-wrap: wrap; } .nl-inline-form input[type="email"] { min-width: 200px; } } </style> <script> (function() { var form = document.getElementById('nl-inline-form'); if (!form) return; var LANG = 'en'; var API_URL = 'https://seoclaims.com/api/subscribe'; var COOKIE = 'nl_subscribed'; var MSG = { success: 'You\'re in! You\'ll receive the next analyses.', already: 'You are already subscribed.', error: 'Something went wrong, please try again.', }; function getCookie(n) { return document.cookie.split(';').some(function(c) { return c.trim().startsWith(n + '='); }); } function setCookie(n, days) { var d = new Date(); d.setDate(d.getDate() + days); document.cookie = n + '=1;expires=' + d.toUTCString() + ';path=/;SameSite=Lax'; } // Masquer si deja inscrit if (getCookie(COOKIE)) { var section = form.closest('.nl-inline-section'); if (section) section.style.display = 'none'; return; } form.addEventListener('submit', function(e) { e.preventDefault(); var email = form.querySelector('input[type="email"]').value.trim(); var btn = form.querySelector('button'); btn.disabled = true; var orig = btn.textContent; btn.textContent = '...'; fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, lang: LANG }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { setCookie(COOKIE, 365); form.innerHTML = '<div style="color:#0d9488;font-weight:600;font-size:15px;padding:10px 0;">✓ ' + (data.already ? MSG.already : MSG.success) + '</div>'; // Masquer aussi banniere footer et popup si presents var banner = document.getElementById('nl-banner'); var popup = document.getElementById('nl-popup'); if (banner) banner.style.display = 'none'; if (popup) popup.style.display = 'none'; } else { btn.disabled = false; btn.textContent = orig; var err = form.querySelector('.nl-inline-err'); if (!err) { err = document.createElement('div'); err.className = 'nl-inline-err'; err.style.cssText = 'color:#dc2626;font-size:13px;margin-top:6px;'; form.parentNode.insertBefore(err, form.nextSibling); } err.textContent = data.error || MSG.error; } }) .catch(function() { btn.disabled = false; btn.textContent = orig; }); }); })(); </script> <footer class="site-footer"> <div class="footer-inner"> <!-- ====== Row 1 : About + Navigation + Resources ====== --> <div class="footer-top"> <div> <div class="footer-brand"> <picture> <source srcset="https://seoclaims.com/assets/logo.webp" type="image/webp"> <img src="https://seoclaims.com/assets/logo.png" alt="SEO Declarations" class="footer-logo" loading="lazy" width="132" height="72"> </picture> </div> <p>SEO Claims collects, analyzes and translates <a href="/en/declarations">official Google statements</a> about search engine optimization, sourced from <a href="/en/articles">published articles</a> and <a href="/en/videos">YouTube videos</a> by Google Search Central. Each statement is enriched with <a href="/en/declarations">AI analysis</a>, classified by <a href="/en/declarations">SEO category</a> and attributed to its <a href="/en/speaker/john-mueller">author</a>. An essential tool for SEO professionals who want to know exactly what Google recommends.</p> </div> <div> <div class="footer-heading">Navigation</div> <nav class="footer-nav"> <a href="/en/declarations" class="footer-link">Statements</a> <a href="/en/labs/" class="footer-link">Labs SEO</a> <a href="/en/speaker/john-mueller" class="footer-link">Authors</a> <a href="/en/sitemap-declarations" class="footer-link">Sitemap</a> <a href="/en/top-seo-agencies-france" class="footer-link">Top SEO Agencies</a> <a href="/en/legal" class="footer-link">Legal Notice</a> </nav> </div> <div> <div class="footer-heading">Resources</div> <nav class="footer-nav"> <a href="https://search.google.com/search-console" target="_blank" rel="noopener" class="footer-link">Google Search Console</a> <a href="https://pagespeed.web.dev/" target="_blank" rel="noopener" class="footer-link">PageSpeed Insights</a> <a href="https://search.google.com/test/rich-results" target="_blank" rel="noopener" class="footer-link">Rich Results Test</a> <a href="https://developer.chrome.com/docs/lighthouse/" target="_blank" rel="noopener" class="footer-link">Lighthouse</a> <a href="https://developers.google.com/search/docs" target="_blank" rel="noopener" class="footer-link">Google Search Guidelines</a> <a href="/en/google-tools" class="footer-link" style="color: #60a5fa; font-weight: 600;">All Google Tools →</a> </nav> </div> </div> <!-- ====== Row 2 : 3 piliers SEO — Top 5 categories par vues ====== --> <div class="footer-pillars"> <!-- Semantique --> <details class="footer-pillar-details" open> <summary class="footer-pillar-title"> <span style="display:flex;align-items:center;gap:8px;"> <span class="footer-pillar-dot" style="background: #60a5fa;"></span> <span class="footer-pillar-label" style="color: #60a5fa;">Semantic</span> </span> </summary> <nav class="footer-pillar-nav"> <a href="/en/c/ia-seo" class="footer-pillar-link"> <span>AI & SEO</span> <span class="footer-pillar-count">9673</span> </a> <a href="/en/c/contenu" class="footer-pillar-link"> <span>Content</span> <span class="footer-pillar-count">5585</span> </a> <a href="/en/c/nom-domaine" class="footer-pillar-link"> <span>Domain Name</span> <span class="footer-pillar-count">1943</span> </a> <a href="/en/c/pdf-fichiers" class="footer-pillar-link"> <span>PDF & Files</span> <span class="footer-pillar-count">497</span> </a> <a href="/en/c/discover-actualites" class="footer-pillar-link"> <span>Discover & News</span> <span class="footer-pillar-count">343</span> </a> </nav> </details> <!-- Technique --> <details class="footer-pillar-details" open> <summary class="footer-pillar-title"> <span style="display:flex;align-items:center;gap:8px;"> <span class="footer-pillar-dot" style="background: #34d399;"></span> <span class="footer-pillar-label" style="color: #34d399;">Technical</span> </span> </summary> <nav class="footer-pillar-nav"> <a href="/en/c/anciennete-historique" class="footer-pillar-link"> <span>Domain Age & History</span> <span class="footer-pillar-count">6840</span> </a> <a href="/en/c/crawl-indexation" class="footer-pillar-link"> <span>Crawl & Indexing</span> <span class="footer-pillar-count">3560</span> </a> <a href="/en/c/javascript-technique" class="footer-pillar-link"> <span>JavaScript & Technical SEO</span> <span class="footer-pillar-count">2358</span> </a> <a href="/en/c/search-console" class="footer-pillar-link"> <span>Search Console</span> <span class="footer-pillar-count">1848</span> </a> <a href="/en/c/performance-web" class="footer-pillar-link"> <span>Web Performance</span> <span class="footer-pillar-count">105</span> </a> </nav> </details> <!-- Autorite --> <details class="footer-pillar-details" open> <summary class="footer-pillar-title"> <span style="display:flex;align-items:center;gap:8px;"> <span class="footer-pillar-dot" style="background: #fbbf24;"></span> <span class="footer-pillar-label" style="color: #fbbf24;">Authority</span> </span> </summary> <nav class="footer-pillar-nav"> <a href="/en/c/liens-backlinks" class="footer-pillar-link"> <span>Links & Backlinks</span> <span class="footer-pillar-count">2076</span> </a> <a href="/en/c/reseaux-sociaux" class="footer-pillar-link"> <span>Social Media</span> <span class="footer-pillar-count">541</span> </a> <a href="/en/c/penalites-spam" class="footer-pillar-link"> <span>Penalties & Spam</span> <span class="footer-pillar-count">515</span> </a> <a href="/en/c/algorithmes" class="footer-pillar-link"> <span>Algorithms</span> <span class="footer-pillar-count">416</span> </a> <a href="/en/c/recherche-locale" class="footer-pillar-link"> <span>Local Search</span> <span class="footer-pillar-count">116</span> </a> </nav> </details> </div> <!-- ====== Row 3 : Dernieres prises de parole ====== --> <div class="footer-popular"> <div class="footer-popular-title">Latest Google statements on SEO</div> <div class="footer-popular-grid"> <a href="/en/d/celui-qui-dit-tout-savoir-sur-le-seo-est-un-imposteur" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">John Mueller</span> </span> <span class="footer-popular-text">Pourquoi personne ne peut vraiment maîtriser le SEO à 100% ?</span> </a> <a href="/en/d/le-seo-est-complexe-multiforme-et-resilient" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">John Mueller</span> </span> <span class="footer-popular-text">Peut-on vraiment se permettre de faire n'importe quoi en SEO sans conséq…</span> </a> <a href="/en/d/usage-de-custom-javascript-metrics" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Martin Splitt</span> </span> <span class="footer-popular-text">Google utilise-t-il des scripts JavaScript personnalisés pour évaluer vo…</span> </a> <a href="/en/d/requete-sql-dans-bigquery-pour-l-analyse-seo" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Gary Illyes</span> </span> <span class="footer-popular-text">Faut-il vraiment maîtriser SQL et BigQuery pour faire du SEO en 2025 ?</span> </a> <a href="/en/d/recommandations-sur-la-taille-des-fichiers-robots-txt" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Martin Splitt</span> </span> <span class="footer-popular-text">Faut-il vraiment respecter la limite de 100KB pour votre fichier robots.…</span> </a> <a href="/en/d/importancia-d-http-archive-pour-le-seo" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Gary Illyes</span> </span> <span class="footer-popular-text">HTTP Archive : Google révèle-t-il enfin comment il analyse vraiment vos …</span> </a> <a href="/en/d/utilisation-de-bigquery-pour-analyser-les-sites-web" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Martin Splitt</span> </span> <span class="footer-popular-text">BigQuery est-il vraiment indispensable pour analyser vos données SEO à g…</span> </a> <a href="/en/d/nouvelle-collecte-de-donnees-robots-txt-avec-http-archive" class="footer-popular-link"> <span class="footer-popular-meta"> <span class="footer-popular-date">Apr 2026</span> <span class="footer-popular-speaker">Gary Illyes</span> </span> <span class="footer-popular-text">Pourquoi Google publie-t-il soudainement des données massives sur l'usag…</span> </a> </div> </div> <!-- ====== Row 4 : Copyright ====== --> <div class="footer-bottom"> <span>© 2026 SEO Declarations. All rights reserved.</span> <span>This site is not affiliated with Google. Statements presented are from public Google communications.</span> </div> </div> </footer> <!-- ====== Popup Newsletter (une fois par session) ====== --> <div class="nl-popup-overlay" id="nl-popup" style="display:none;" role="dialog" aria-modal="true" aria-label="Get a complete real-time analysis of the latest Google SEO declarations"> <div class="nl-popup-box"> <button class="nl-popup-close" id="nl-popup-close" aria-label="Close">×</button> <div class="nl-popup-header"> <div class="nl-popup-badge">Stay ahead</div> <h3 class="nl-popup-title">Get a complete real-time analysis of the latest Google SEO declarations</h3> <p class="nl-popup-sub">Be the first to know every time a new official Google SEO statement drops, with full analysis included.</p> </div> <form class="nl-form nl-popup-form" data-lang="en" id="nl-popup-form"> <div class="nl-popup-input-group"> <input type="email" name="email" placeholder="Your email address" required autocomplete="email"> <button type="submit">Subscribe for free</button> </div> </form> <div class="nl-popup-trust"> <span class="nl-popup-trust-icon">🔒</span> <span>No spam. Unsubscribe in one click.</span> </div> </div> </div> <style> /* === Newsletter Popup === */ .nl-popup-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 20px; animation: nlFadeIn .3s ease; } .nl-popup-box { background: linear-gradient(165deg, #ffffff 0%, #f8fafc 100%); border-radius: 20px; padding: 40px 36px 32px; max-width: 460px; width: 100%; position: relative; box-shadow: 0 25px 80px rgba(0,0,0,.2), 0 0 0 1px rgba(0,0,0,.05); animation: nlSlideUp .4s cubic-bezier(.16,1,.3,1); text-align: center; } .nl-popup-close { position: absolute; top: 12px; right: 14px; background: none; border: none; font-size: 22px; color: #94a3b8; cursor: pointer; line-height: 1; padding: 6px 10px; border-radius: 8px; transition: color .2s, background .2s; } .nl-popup-close:hover { color: #374151; background: #f1f5f9; } .nl-popup-header { margin-bottom: 24px; } .nl-popup-badge { display: inline-block; background: linear-gradient(135deg, #0ea5e9, #7c3aed); color: #fff; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; padding: 5px 14px; border-radius: 20px; margin-bottom: 16px; } .nl-popup-title { font-size: 22px; font-weight: 800; color: #0f172a; line-height: 1.3; margin: 0 0 10px; } .nl-popup-sub { font-size: 14px; color: #64748b; line-height: 1.6; margin: 0; } .nl-popup-input-group { display: flex; gap: 0; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.06); } .nl-popup-form input[type="email"] { flex: 1; padding: 14px 16px; background: #fff; color: #1e293b; border: none; font-size: 15px; outline: none; min-width: 0; } .nl-popup-form input[type="email"]::placeholder { color: #94a3b8; } .nl-popup-form button { padding: 14px 22px; background: linear-gradient(135deg, #0ea5e9, #6d28d9); color: #fff; border: none; font-size: 14px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: opacity .2s; } .nl-popup-form button:hover { opacity: .9; } .nl-popup-form button:disabled { opacity: .5; cursor: default; } .nl-popup-trust { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 14px; font-size: 12px; color: #94a3b8; } .nl-popup-trust-icon { font-size: 13px; } .nl-popup-msg { padding: 16px; border-radius: 12px; font-weight: 600; font-size: 15px; margin-top: 4px; } .nl-popup-msg.success { background: #ecfdf5; color: #059669; } .nl-popup-msg.error { background: #fef2f2; color: #dc2626; } @media (max-width: 480px) { .nl-popup-box { padding: 32px 20px 24px; } .nl-popup-input-group { flex-direction: column; border-radius: 12px; } .nl-popup-form input[type="email"] { border-bottom: 1px solid #e2e8f0; } .nl-popup-form button { padding: 14px; border-radius: 0 0 12px 12px; } .nl-popup-title { font-size: 19px; } } @keyframes nlFadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes nlSlideUp { from { transform: translateY(30px) scale(.97); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } </style> <script> (function() { var LANG = 'en'; var API_URL = 'https://seoclaims.com/api/subscribe'; var COOKIE = 'nl_subscribed'; var SESSION_KEY = 'nl_popup_shown'; var MSG = { success: 'You\'re in! Check your inbox.', already: 'You are already subscribed.', error: 'Something went wrong, please try again.', }; function getCookie(n) { return document.cookie.split(';').some(c => c.trim().startsWith(n + '=')); } function setCookie(n, days) { var d = new Date(); d.setDate(d.getDate() + days); document.cookie = n + '=1;expires=' + d.toUTCString() + ';path=/;SameSite=Lax'; } function closePopup(popup) { popup.style.opacity = '0'; setTimeout(function() { popup.style.display = 'none'; }, 300); sessionStorage.setItem(SESSION_KEY, '1'); } function showMsg(container, text, type) { var existing = container.querySelector('.nl-popup-msg'); if (existing) existing.remove(); var el = document.createElement('div'); el.className = 'nl-popup-msg ' + type; el.textContent = text; container.appendChild(el); } document.addEventListener('DOMContentLoaded', function() { var popup = document.getElementById('nl-popup'); var popupForm = document.getElementById('nl-popup-form'); var closeBtn = document.getElementById('nl-popup-close'); if (!popup || !popupForm) return; // Ne pas afficher si : deja inscrit OU deja vu cette session if (getCookie(COOKIE) || sessionStorage.getItem(SESSION_KEY)) return; // Afficher apres 30% scroll OU 20 secondes var nlShown = false; function showNlPopup() { if (nlShown) return; nlShown = true; popup.style.display = 'flex'; sessionStorage.setItem(SESSION_KEY, '1'); } var scrollHandler = function() { var pct = window.scrollY / (document.body.scrollHeight - window.innerHeight); if (pct > 0.3) { showNlPopup(); window.removeEventListener('scroll', scrollHandler); } }; window.addEventListener('scroll', scrollHandler, {passive: true}); setTimeout(showNlPopup, 20000); closeBtn.addEventListener('click', function() { closePopup(popup); }); popup.addEventListener('click', function(e) { if (e.target === popup) closePopup(popup); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && popup.style.display === 'flex') closePopup(popup); }); popupForm.addEventListener('submit', function(e) { e.preventDefault(); var email = popupForm.querySelector('input[type="email"]').value.trim(); var btn = popupForm.querySelector('button[type="submit"]'); btn.disabled = true; var origText = btn.textContent; btn.textContent = '...'; fetch(API_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email: email, lang: LANG}) }) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.textContent = origText; if (data.success) { setCookie(COOKIE, 365); popupForm.style.display = 'none'; showMsg(popup.querySelector('.nl-popup-box'), data.already ? MSG.already : MSG.success, 'success'); // Masquer aussi le bloc inline si present var inline = document.querySelector('.nl-inline-section'); if (inline) inline.style.display = 'none'; setTimeout(function() { closePopup(popup); }, 2500); } else { showMsg(popup.querySelector('.nl-popup-box'), data.error || MSG.error, 'error'); } }) .catch(function() { btn.disabled = false; btn.textContent = origText; showMsg(popup.querySelector('.nl-popup-box'), MSG.error, 'error'); }); }); }); })(); </script> <!-- WebSite + Organization Schema --> <script type="application/ld+json"> { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://seoclaims.com/#website", "name": "SEO Declarations", "url": "https://seoclaims.com", "inLanguage": [ "fr-FR", "en" ], "publisher": { "@id": "https://seoclaims.com/#organization" }, "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", "urlTemplate": "https://seoclaims.com/?q={search_term_string}" }, "query-input": "required name=search_term_string" } }, { "@type": "Organization", "@id": "https://seoclaims.com/#organization", "name": "SEO Declarations", "url": "https://seoclaims.com", "logo": { "@type": "ImageObject", "url": "https://seoclaims.com/assets/logo.png", "width": 512, "height": 512 }, "description": "Search engine for official Google SEO declarations, bilingual analysis and AI-enriched context.", "knowsAbout": [ "Search Engine Optimization", "Google Search", "Core Web Vitals", "Technical SEO", "Content SEO", "Link building", "Structured data", "AI search optimization" ], "sameAs": [] } ] }</script> <!-- ==================== CHATBOT WIDGET ==================== --> <button id="cbw-launcher" type="button" aria-label="Ask the SEO assistant a question" title="Ask the SEO assistant a question"> <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> </svg> <span class="cbw-launcher-label">SEO Assistant</span> <span class="cbw-launcher-pulse" aria-hidden="true"></span> </button> <div id="cbw-panel" role="dialog" aria-label="SEO Assistant" aria-hidden="true"> <div class="cbw-head"> <div class="cbw-head-txt"> <strong>SEO Assistant</strong> <span>Powered by official Google declarations</span> </div> <div class="cbw-head-actions"> <a href="https://seoclaims.com/en/chatbot" class="cbw-icon-btn" title="Open full version" aria-label="Open full version"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"></path></svg> </a> <button type="button" id="cbw-close" class="cbw-icon-btn" title="Close" aria-label="Close"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18"></path><path d="M6 6l12 12"></path></svg> </button> </div> </div> <div class="cbw-body" id="cbw-messages" aria-live="polite"> <div class="cbw-msg assistant cbw-welcome">Hi! Ask me anything about SEO and Google — I answer with cited sources from official declarations.</div> </div> <form class="cbw-form" id="cbw-form" autocomplete="off"> <textarea id="cbw-input" rows="1" placeholder="Ask a question..." maxlength="500" required></textarea> <button type="submit" id="cbw-send" aria-label="Send"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> </button> </form> </div> <style> #cbw-launcher { position: fixed; bottom: 24px; right: 24px; z-index: 9998; display: flex; align-items: center; gap: 8px; padding: 14px 20px 14px 16px; background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: #fff; border: none; border-radius: 999px; box-shadow: 0 8px 24px rgba(124,58,237,.35); font-family: inherit; font-size: .93rem; font-weight: 600; cursor: pointer; transition: transform .15s ease, box-shadow .15s ease; } #cbw-launcher:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(124,58,237,.45); } #cbw-launcher svg { flex-shrink: 0; } .cbw-launcher-label { white-space: nowrap; } .cbw-launcher-pulse { position: absolute; top: -2px; right: -2px; width: 12px; height: 12px; background: #10b981; border: 2px solid #fff; border-radius: 50%; animation: cbwPulse 2s infinite; } @keyframes cbwPulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: .7; } } #cbw-panel { position: fixed; bottom: 24px; right: 24px; z-index: 9999; width: 400px; max-width: calc(100vw - 32px); height: 560px; max-height: calc(100vh - 48px); background: #fff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,.2); display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; transform: translateY(20px) scale(.96); opacity: 0; pointer-events: none; transition: transform .22s ease, opacity .22s ease; font-family: inherit; } #cbw-panel[aria-hidden="false"] { transform: translateY(0) scale(1); opacity: 1; pointer-events: auto; } #cbw-panel[aria-hidden="false"] ~ #cbw-launcher, body.cbw-open #cbw-launcher { opacity: 0; pointer-events: none; transform: scale(.8); } .cbw-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; background: linear-gradient(135deg, #7c3aed, #5b21b6); color: #fff; } .cbw-head-txt strong { display: block; font-size: .97rem; line-height: 1.2; } .cbw-head-txt span { display: block; font-size: .75rem; opacity: .85; margin-top: 2px; } .cbw-head-actions { display: flex; gap: 6px; } .cbw-icon-btn { width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,.12); color: #fff; border: none; border-radius: 8px; cursor: pointer; text-decoration: none; transition: background .15s; } .cbw-icon-btn:hover { background: rgba(255,255,255,.22); } .cbw-body { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; background: #fafafa; } .cbw-msg { max-width: 92%; padding: 10px 14px; border-radius: 12px; font-size: .9rem; line-height: 1.55; animation: cbwIn .2s ease-out; } @keyframes cbwIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } .cbw-msg.user { align-self: flex-end; background: linear-gradient(135deg, #1a73e8, #1557b0); color: #fff; border-bottom-right-radius: 3px; } .cbw-msg.assistant { align-self: flex-start; background: #fff; border: 1px solid #e2e8f0; border-bottom-left-radius: 3px; color: #1e293b; } .cbw-msg.assistant.error { background: #fef2f2; border-color: #fecaca; color: #991b1b; } .cbw-msg.cbw-welcome { background: linear-gradient(135deg, #f3e8ff, #ede9fe); border-color: #c4b5fd; color: #5b21b6; } .cbw-citation { display: inline-block; background: #ede9fe; color: #5b21b6; font-size: .75rem; font-weight: 700; padding: 0 6px; border-radius: 8px; margin: 0 2px; text-decoration: none; vertical-align: 1px; } .cbw-hint { display: inline-block; background: #fef3c7; color: #92400e; font-style: normal; font-size: .82rem; padding: 1px 7px; border-radius: 6px; margin-left: 4px; font-weight: 500; } .cbw-citation:hover { background: #ddd6fe; } .cbw-sources { margin-top: 10px; padding-top: 9px; border-top: 1px dashed #cbd5e1; } .cbw-sources-title { font-size: .7rem; text-transform: uppercase; letter-spacing: .4px; font-weight: 700; color: #64748b; margin-bottom: 6px; } .cbw-src { display: block; padding: 8px 10px; background: #fff; border: 1px solid #e2e8f0; border-left: 3px solid #7c3aed; border-radius: 6px; margin-bottom: 5px; font-size: .78rem; color: inherit; text-decoration: none; transition: transform .12s; } .cbw-src:hover { transform: translateX(2px); border-color: #7c3aed; } .cbw-src strong { display: block; color: #1e293b; margin-bottom: 1px; font-size: .8rem; line-height: 1.3; } .cbw-src .cbw-src-meta { color: #64748b; font-size: .7rem; } .cbw-src .cbw-src-id { display: inline-block; background: #ede9fe; color: #5b21b6; font-family: monospace; font-size: .68rem; font-weight: 700; padding: 0 5px; border-radius: 4px; margin-right: 4px; } .cbw-typing { display: flex; gap: 4px; padding: 10px 14px; } .cbw-typing span { width: 7px; height: 7px; border-radius: 50%; background: #7c3aed; opacity: .5; animation: cbwTyping 1.2s infinite; } .cbw-typing span:nth-child(2) { animation-delay: .15s; } .cbw-typing span:nth-child(3) { animation-delay: .3s; } @keyframes cbwTyping { 0%,80%,100% { transform: scale(.7); opacity: .4; } 40% { transform: scale(1); opacity: 1; } } .cbw-form { display: flex; gap: 8px; padding: 12px; background: #fff; border-top: 1px solid #e2e8f0; } .cbw-form textarea { flex: 1; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 10px; font-size: .9rem; font-family: inherit; resize: none; min-height: 40px; max-height: 110px; outline: none; transition: border-color .15s; } .cbw-form textarea:focus { border-color: #7c3aed; box-shadow: 0 0 0 3px rgba(124,58,237,.12); } .cbw-form button { width: 42px; height: 42px; display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #7c3aed, #5b21b6); color: #fff; border: none; border-radius: 10px; cursor: pointer; flex-shrink: 0; } .cbw-form button:disabled { opacity: .5; cursor: not-allowed; } @media (max-width: 640px) { #cbw-launcher { bottom: 16px; right: 16px; padding: 12px 16px 12px 14px; font-size: .85rem; } .cbw-launcher-label { display: none; } #cbw-launcher { padding: 14px; } #cbw-panel { top: 0; bottom: 0; left: 0; right: 0; width: 100vw; max-width: 100vw; /* dvh = dynamic viewport height : se reduit quand le clavier mobile s'ouvre */ /* fallback vh pour navigateurs tres anciens */ height: 100vh; height: 100dvh; max-height: 100vh; max-height: 100dvh; border-radius: 0; } /* Laisse le clavier rogner l'espace du body, pas de la frame entiere */ .cbw-body { overscroll-behavior: contain; } } @media print { #cbw-launcher, #cbw-panel { display: none !important; } } </style> <script> (function() { const LANG = "en"; const T = {"sending":"...","send":"Send","sources":"Sources","rate_limit":"Limit of 10 questions per hour reached.","error":"Error. Please try again.","too_short":"Question too short."}; const launcher = document.getElementById('cbw-launcher'); const panel = document.getElementById('cbw-panel'); const closeBtn = document.getElementById('cbw-close'); const msgsEl = document.getElementById('cbw-messages'); const formEl = document.getElementById('cbw-form'); const inputEl = document.getElementById('cbw-input'); const sendBtn = document.getElementById('cbw-send'); // Sync panel height to the visual viewport (keyboard-aware) sur mobile // Necessaire car Chrome Android n'honore pas toujours 100dvh correctement // quand le clavier virtuel s'ouvre : la panel reste a 100vh et le contenu // du haut est pousse hors ecran. function syncViewport() { if (!window.visualViewport) return; if (panel.getAttribute('aria-hidden') !== 'false') return; if (window.innerWidth > 640) { panel.style.height = ''; return; } panel.style.height = window.visualViewport.height + 'px'; } function open() { panel.setAttribute('aria-hidden', 'false'); document.body.classList.add('cbw-open'); syncViewport(); if (window.visualViewport) { window.visualViewport.addEventListener('resize', syncViewport); window.visualViewport.addEventListener('scroll', syncViewport); } // Afficher le message de bienvenue au chargement msgsEl.scrollTop = 0; setTimeout(() => inputEl.focus(), 200); } function close() { panel.setAttribute('aria-hidden', 'true'); document.body.classList.remove('cbw-open'); panel.style.height = ''; if (window.visualViewport) { window.visualViewport.removeEventListener('resize', syncViewport); window.visualViewport.removeEventListener('scroll', syncViewport); } } launcher.addEventListener('click', open); closeBtn.addEventListener('click', close); document.addEventListener('keydown', e => { if (e.key === 'Escape' && panel.getAttribute('aria-hidden') === 'false') close(); }); function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function renderAnswer(answer, sources, queryId) { const byId = {}; (sources || []).forEach(s => { byId[s.id] = s; }); let html = esc(answer).replace(/\[#(\d+)\]/g, (m, id) => { const s = byId[id]; if (!s) return ''; return '<a class="cbw-citation" href="' + esc(s.url) + '" target="_blank" rel="noopener" data-qid="' + (queryId||0) + '" data-sid="' + id + '" data-kind="citation">#' + id + '</a>'; }); // Markdown italique *texte* -> <em>texte</em> (non greedy, non multiligne) html = html.replace(/\*([^*\n]+?)\*/g, '<em class="cbw-hint">$1</em>'); return html.replace(/\n/g, '<br>'); } function renderSources(sources, queryId) { if (!sources || !sources.length) return ''; let out = '<div class="cbw-sources"><div class="cbw-sources-title">' + esc(T.sources) + '</div>'; sources.forEach(s => { const meta = [s.speaker, s.date].filter(Boolean).join(' · '); out += '<a class="cbw-src" href="' + esc(s.url) + '" target="_blank" rel="noopener" data-qid="' + (queryId||0) + '" data-sid="' + s.id + '" data-kind="source_card">' + '<strong><span class="cbw-src-id">#' + s.id + '</span>' + esc(s.title || '') + '</strong>' + '<div class="cbw-src-meta">' + meta + '</div></a>'; }); return out + '</div>'; } // Beacon de clic : non bloquant, n'empeche pas la navigation function logClick(qid, sid, kind) { if (!qid || !sid) return; const payload = JSON.stringify({ query_id: qid, source_id: sid, kind }); try { if (navigator.sendBeacon) { navigator.sendBeacon('/api/chatbot-click', new Blob([payload], {type: 'application/json'})); } else { fetch('/api/chatbot-click', { method: 'POST', headers: {'Content-Type':'application/json'}, body: payload, keepalive: true }).catch(() => {}); } } catch (e) {} } // Delegation d'evenement sur le conteneur messages msgsEl.addEventListener('click', function(e) { const a = e.target.closest('a[data-qid][data-sid]'); if (!a) return; logClick(parseInt(a.dataset.qid, 10), parseInt(a.dataset.sid, 10), a.dataset.kind || 'source_card'); }); function addUserMsg(txt) { const d = document.createElement('div'); d.className = 'cbw-msg user'; d.innerHTML = esc(txt).replace(/\n/g, '<br>'); msgsEl.appendChild(d); scroll(); } function addBot(data, isError) { const d = document.createElement('div'); d.className = 'cbw-msg assistant' + (isError ? ' error' : ''); if (typeof data === 'string') d.innerHTML = esc(data); else { const qid = data.query_id || 0; d.innerHTML = renderAnswer(data.answer || '', data.sources || [], qid) + renderSources(data.sources || [], qid); } msgsEl.appendChild(d); // Pas de scroll : on laisse l'utilisateur sur sa question, il scrolle lui-meme pour lire la reponse } function showTyping() { const d = document.createElement('div'); d.className = 'cbw-msg assistant'; d.id = 'cbw-typing'; d.innerHTML = '<div class="cbw-typing"><span></span><span></span><span></span></div>'; msgsEl.appendChild(d); // Pas de scroll sur l'indicateur de frappe non plus } function hideTyping() { const t = document.getElementById('cbw-typing'); if (t) t.remove(); } function scroll() { msgsEl.scrollTop = msgsEl.scrollHeight; } async function ask(question, origin) { origin = origin || 'widget'; addUserMsg(question); inputEl.value = ''; inputEl.style.height = 'auto'; sendBtn.disabled = true; showTyping(); try { const resp = await fetch('/api/chatbot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, lang: LANG, referer: window.location.pathname, origin }) }); hideTyping(); const data = await resp.json(); if (!resp.ok) { addBot(data.error === 'rate_limit' ? T.rate_limit : T.error, true); } else { addBot(data, false); } } catch (e) { hideTyping(); addBot(T.error, true); } finally { sendBtn.disabled = false; inputEl.focus(); } } // API publique pour declencher le chatbot depuis d'autres pages // Usage : window.ChatbotSEO.ask("Ma question", "search_fallback") window.ChatbotSEO = { open, close, ask: function(question, origin) { if (!question || question.length < 3) return; open(); // Laisser l'animation d'ouverture se lancer puis envoyer setTimeout(() => ask(question, origin || 'search_fallback'), 350); } }; inputEl.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 110) + 'px'; }); inputEl.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); formEl.requestSubmit(); } }); formEl.addEventListener('submit', function(e) { e.preventDefault(); const q = inputEl.value.trim(); if (q.length < 3) { addBot(T.too_short, true); return; } ask(q); }); })(); </script> <nav class="mobile-nav" aria-label="Mobile navigation"> <a href="/en/" class="mobile-nav-item"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> <span>Search</span> </a> <a href="/en/declarations" class="mobile-nav-item"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg> <span>Categories</span> </a> <a href="/en/?browse=1&sort=date_desc" class="mobile-nav-item"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <span>Recent</span> </a> <a href="/" class="mobile-nav-item mobile-nav-lang"> <svg width="22" height="16" viewBox="0 0 640 480"> <rect width="213" height="480" fill="#002654"/><rect x="213" width="214" height="480" fill="#fff"/><rect x="427" width="213" height="480" fill="#ce1126"/> </svg> <span>FR</span> </a> </nav> <!-- Lightbox (self-contained) --> <div id="lightbox" style="display:none;position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.9);align-items:center;justify-content:center;padding:16px;cursor:zoom-out;" onclick="closeLightbox()"> <button onclick="closeLightbox();event.stopPropagation();" style="position:absolute;top:12px;right:16px;z-index:100000;width:44px;height:44px;border:none;background:rgba(255,255,255,.2);color:#fff;font-size:1.6rem;border-radius:50%;cursor:pointer;" aria-label="Fermer">×</button> <img id="lightbox-img" src="" alt="" style="max-width:95%;max-height:90vh;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,.5);" onclick="event.stopPropagation()"> </div> <script> function openLightbox(src){ var lb=document.getElementById('lightbox'); document.getElementById('lightbox-img').src=src; lb.style.display='flex'; document.body.style.overflow='hidden'; } function closeLightbox(){ document.getElementById('lightbox').style.display='none'; document.body.style.overflow=''; } document.addEventListener('keydown',function(e){if(e.key==='Escape')closeLightbox();}); function ytPlay(card) { var vid = card.dataset.vid; var ts = parseInt(card.dataset.ts) || 0; card.classList.add('playing'); card.innerHTML = '<iframe src="https://www.youtube-nocookie.com/embed/' + vid + '?autoplay=1&rel=0&modestbranding=1' + (ts > 0 ? '&start=' + ts : '') + '" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>'; } </script> </body> </html>