PageSourceSearch

https://seo-snippet-previewer-fr.pages.dev/script.js

js seo-snippet-previewer-fr.pages.dev collected 2026-10-03 05:32:24 UTC 13,053 bytes, 369 lines download raw bytes

1const fields = {
2  title: document.getElementById("seo-title"),
3  description: document.getElementById("seo-description"),
4  url: document.getElementById("seo-url")
5};
6
7const outputs = {
8  title: document.getElementById("snippet-title"),
9  description: document.getElementById("snippet-description"),
10  domain: document.getElementById("snippet-domain"),
11  path: document.getElementById("snippet-path"),
12  score: document.getElementById("quality-score"),
13  summary: document.getElementById("quality-summary"),
14  microcopy: document.getElementById("microcopy"),
15  coachList: document.getElementById("coach-list")
16};
17
18const counters = {
19  title: document.getElementById("title-count"),
20  description: document.getElementById("description-count"),
21  url: document.getElementById("url-count")
22};
23
24const statuses = {
25  title: document.getElementById("title-status"),
26  description: document.getElementById("description-status"),
27  url: document.getElementById("url-status")
28};
29
30const actions = {
31  example: document.getElementById("example-button"),
32  clear: document.getElementById("clear-button"),
33  copyTitle: document.getElementById("copy-title"),
34  copyDescription: document.getElementById("copy-description"),
35  copySnippet: document.getElementById("copy-snippet"),
36  share: document.getElementById("share-button"),
37  desktop: document.getElementById("desktop-view"),
38  mobile: document.getElementById("mobile-view"),
39  stage: document.getElementById("preview-stage")
40};
41
42const defaults = {
43  title: "Prévisualisez votre title SEO avant publication",
44  description: "Vérifiez la longueur de votre meta description, affinez votre promesse éditoriale et observez un aperçu réaliste du résultat dans Google.",
45  url: "www.exemple.fr/ressources/snippet-seo"
46};
47
48const exampleData = {
49  title: "Simulateur de title SEO : testez votre snippet Google en direct",
50  description: "Prévisualisez votre title, votre meta description et votre URL pour améliorer la lisibilité, renforcer le CTR et peaufiner votre aperçu référencement.",
51  url: "https://www.mon-site.fr/outils/previsualisateur-title-meta-description"
52};
53
54// Collez votre lien d'affiliation ici plus tard.
55// Tant que cette chaîne reste vide, aucun bouton n'ouvre de lien externe.
56const referralConfig = {
57  url: "",
58  openInNewTab: true
59};
60
61function pluralize(count) {
62  return `${count} caractère${count > 1 ? "s" : ""}`;
63}
64
65function triggerReferral(slot) {
66  if (!referralConfig.url) {
67    return;
68  }
69
70  const separator = referralConfig.url.includes("?") ? "&" : "?";
71  const finalUrl = `${referralConfig.url}${separator}source=${encodeURIComponent(slot)}`;
72
73  if (referralConfig.openInNewTab) {
74    window.open(finalUrl, "_blank", "noopener,noreferrer");
75    return;
76  }
77
78  window.location.href = finalUrl;
79}
80
81function setMeterState(element, type) {
82  element.className = "meter-pill";
83  element.classList.add(`meter-pill--${type}`);
84}
85
86function analyzeLength(length, min, max, label) {
87  if (length === 0) {
88    return {
89      tone: "idle",
90      text: `0 caractère`,
91      hint: `${label} vide pour l’instant.`
92    };
93  }
94
95  if (length < min) {
96    return {
97      tone: "warn",
98      text: pluralize(length),
99      hint: `${label} encore un peu court.`
100    };
101  }
102
103  if (length > max) {
104    return {
105      tone: "bad",
106      text: pluralize(length),
107      hint: `${label} potentiellement trop long.`
108    };
109  }
110
111  return {
112    tone: "good",
113    text: pluralize(length),
114    hint: `${label} dans une zone confortable.`
115  };
116}
117
118function normalizeUrl(value) {
119  const trimmed = value.trim();
120  if (!trimmed) {
121    return defaults.url;
122  }
123
124  const prefixed = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
125
126  try {
127    const parsed = new URL(prefixed);
128    const domain = parsed.hostname.replace(/^www\./i, "") || "exemple.fr";
129    const rawPath = parsed.pathname.replace(/^\/+|\/+$/g, "");
130    const path = rawPath ? rawPath.split("/").slice(0, 3).join(" / ") : "page";
131
132    return {
133      visibleDomain: `www.${domain}`,
134      visiblePath: path,
135      plain: trimmed
136    };
137  } catch (error) {
138    return {
139      visibleDomain: trimmed.replace(/^https?:\/\//i, "").split("/")[0] || "www.exemple.fr",
140      visiblePath: trimmed.split("/").slice(1, 4).join(" / ") || "page",
141      plain: trimmed
142    };
143  }
144}
145
146function collectAdvice(title, description, urlLength) {
147  const notes = [];
148
149  if (title.length < 35) {
150    notes.push("Le title semble un peu court : ajoutez un angle ou un bénéfice concret.");
151  }
152  if (title.length > 60) {
153    notes.push("Le title dépasse la zone souvent confortable : raccourcissez sans perdre la promesse.");
154  }
155  if (title && !/[0-9:?|!-]/.test(title) && title.split(" ").length < 6) {
156    notes.push("Le title reste assez générique : précisez le contexte ou le résultat attendu.");
157  }
158  if (description.length < 90) {
159    notes.push("La meta description peut être plus informative avec un bénéfice ou un usage précis.");
160  }
161  if (description.length > 160) {
162    notes.push("La description risque d’être tronquée : gardez le message principal au début.");
163  }
164  if (description && !/(gratuit|guide|outil|simuler|prévisualiser|améliorer|optimiser|rapide|facile|simple)/i.test(description)) {
165    notes.push("Ajoutez une promesse plus tangible pour renforcer l’intérêt du clic.");
166  }
167  if (urlLength > 55) {
168    notes.push("L’URL paraît longue : un slug plus court facilite la lecture du snippet.");
169  }
170
171  if (!notes.length) {
172    notes.push("Le title paraît lisible et la promesse reste identifiable dès les premiers mots.");
173    notes.push("La description complète correctement le title sans le paraphraser mot pour mot.");
174    notes.push("L’ensemble forme un snippet cohérent et crédible pour un test rapide.");
175  }
176
177  return notes.slice(0, 3);
178}
179
180function computeScore(title, description, urlLength) {
181  let score = 48;
182
183  if (title.length >= 45 && title.length <= 60) score += 16;
184  else if (title.length > 0) score += 8;
185
186  if (description.length >= 120 && description.length <= 160) score += 16;
187  else if (description.length > 0) score += 8;
188
189  if (urlLength >= 10 && urlLength <= 52) score += 10;
190  else if (urlLength > 0) score += 5;
191
192  if (/(comment|guide|outil|simuler|prévisualiser|optimiser|améliorer|aperçu)/i.test(title)) score += 5;
193  if (/(bénéfice|ctr|lisibilité|google|seo|snippet|résultat|description)/i.test(description)) score += 5;
194
195  return Math.min(score, 100);
196}
197
198function scoreSummary(score) {
199  if (score >= 85) {
200    return "Snippet solide : le message est clair, cohérent et prêt pour une validation éditoriale.";
201  }
202  if (score >= 70) {
203    return "Base convaincante : quelques ajustements peuvent encore renforcer le clic et la précision.";
204  }
205  if (score >= 55) {
206    return "Un point de départ correct. Vous pouvez encore renforcer la clarté et l’intérêt du clic.";
207  }
208  return "Le snippet mérite une reformulation : travaillez surtout la promesse, la précision et la lisibilité.";
209}
210
211function refreshMicrocopy(title, description) {
212  if (!title && !description) {
213    outputs.microcopy.textContent = "Commencez avec un title précis : la prévisualisation s’actualise immédiatement.";
214    return;
215  }
216
217  if (title && !description) {
218    outputs.microcopy.textContent = "Bon début. Ajoutez maintenant une meta description qui complète le title sans le répéter.";
219    return;
220  }
221
222  if (title && description) {
223    outputs.microcopy.textContent = "Votre snippet prend forme. Vérifiez surtout la promesse, le bénéfice et le confort de lecture.";
224    return;
225  }
226
227  outputs.microcopy.textContent = "La description progresse. Un title plus spécifique aidera à cadrer l’aperçu.";
228}
229
230function updatePreview() {
231  const titleValue = fields.title.value.trim();
232  const descriptionValue = fields.description.value.trim();
233  const urlValue = fields.url.value.trim();
234
235  const visibleTitle = titleValue || defaults.title;
236  const visibleDescription = descriptionValue || defaults.description;
237  const normalizedUrl = normalizeUrl(urlValue);
238
239  outputs.title.textContent = visibleTitle;
240  outputs.description.textContent = visibleDescription;
241  outputs.domain.textContent = normalizedUrl.visibleDomain;
242  outputs.path.textContent = normalizedUrl.visiblePath;
243
244  const titleAnalysis = analyzeLength(titleValue.length, 35, 60, "Title");
245  const descriptionAnalysis = analyzeLength(descriptionValue.length, 90, 160, "Description");
246  const urlAnalysis = analyzeLength(urlValue.length, 12, 55, "URL");
247
248  counters.title.textContent = titleAnalysis.text;
249  counters.description.textContent = descriptionAnalysis.text;
250  counters.url.textContent = urlAnalysis.text;
251
252  statuses.title.textContent = titleValue ? titleAnalysis.hint : "Longueur neutre pour commencer.";
253  statuses.description.textContent = descriptionValue ? descriptionAnalysis.hint : "Ajoutez une promesse claire et un angle utile.";
254  statuses.url.textContent = urlValue ? urlAnalysis.hint : "Une URL claire aide à la lecture du résultat.";
255
256  setMeterState(counters.title, titleAnalysis.tone);
257  setMeterState(counters.description, descriptionAnalysis.tone);
258  setMeterState(counters.url, urlAnalysis.tone);
259
260  const advice = collectAdvice(titleValue, descriptionValue, urlValue.length);
261  outputs.coachList.innerHTML = advice.map((item) => `<li>${item}</li>`).join("");
262
263  const score = computeScore(titleValue, descriptionValue, urlValue.length);
264  outputs.score.textContent = String(score);
265  outputs.summary.textContent = scoreSummary(score);
266
267  refreshMicrocopy(titleValue, descriptionValue);
268}
269
270async function copyText(text, successMessage) {
271  try {
272    await navigator.clipboard.writeText(text);
273    outputs.microcopy.textContent = successMessage;
274  } catch (error) {
275    outputs.microcopy.textContent = "La copie a échoué sur ce navigateur. Sélectionnez le texte manuellement.";
276  }
277}
278
279function buildShareText() {
280  const titleValue = fields.title.value.trim() || defaults.title;
281  const descriptionValue = fields.description.value.trim() || defaults.description;
282  const urlValue = fields.url.value.trim() || defaults.url;
283
284  return `Title : ${titleValue}\nMeta description : ${descriptionValue}\nURL : ${urlValue}`;
285}
286
287actions.example.addEventListener("click", () => {
288  fields.title.value = exampleData.title;
289  fields.description.value = exampleData.description;
290  fields.url.value = exampleData.url;
291  updatePreview();
292  outputs.microcopy.textContent = "Exemple chargé. Ajustez maintenant la promesse pour l’adapter à votre page.";
293  triggerReferral("example");
294});
295
296actions.clear.addEventListener("click", () => {
297  fields.title.value = "";
298  fields.description.value = "";
299  fields.url.value = "";
300  updatePreview();
301  outputs.microcopy.textContent = "Les champs ont été vidés. Vous pouvez repartir sur une nouvelle idée.";
302  triggerReferral("clear");
303});
304
305actions.copyTitle.addEventListener("click", () => {
306  copyText(fields.title.value.trim() || defaults.title, "Title copié dans le presse-papiers.");
307  triggerReferral("copy-title");
308});
309
310actions.copyDescription.addEventListener("click", () => {
311  copyText(fields.description.value.trim() || defaults.description, "Meta description copiée dans le presse-papiers.");
312  triggerReferral("copy-description");
313});
314
315actions.copySnippet.addEventListener("click", () => {
316  copyText(buildShareText(), "Snippet complet copié dans le presse-papiers.");
317  triggerReferral("copy-snippet");
318});
319
320actions.share.addEventListener("click", async () => {
321  const shareText = buildShareText();
322  const shareData = {
323    title: "Snippet SEO",
324    text: shareText
325  };
326
327  if (navigator.share) {
328    try {
329      await navigator.share(shareData);
330      outputs.microcopy.textContent = "Partage lancé depuis votre appareil.";
331      triggerReferral("share");
332      return;
333    } catch (error) {
334      if (error.name === "AbortError") {
335        outputs.microcopy.textContent = "Partage annulé. Votre contenu reste en place.";
336        return;
337      }
338    }
339  }
340
341  copyText(shareText, "Le snippet a été copié pour être partagé.");
342  triggerReferral("share");
343});
344
345actions.desktop.addEventListener("click", () => {
346  actions.desktop.classList.add("is-active");
347  actions.mobile.classList.remove("is-active");
348  actions.desktop.setAttribute("aria-selected", "true");
349  actions.mobile.setAttribute("aria-selected", "false");
350  actions.stage.classList.remove("preview-stage--mobile");
351  actions.stage.classList.add("preview-stage--desktop");
352  triggerReferral("desktop-view");
353});
354
355actions.mobile.addEventListener("click", () => {
356  actions.mobile.classList.add("is-active");
357  actions.desktop.classList.remove("is-active");
358  actions.mobile.setAttribute("aria-selected", "true");
359  actions.desktop.setAttribute("aria-selected", "false");
360  actions.stage.classList.remove("preview-stage--desktop");
361  actions.stage.classList.add("preview-stage--mobile");
362  triggerReferral("mobile-view");
363});
364
365Object.values(fields).forEach((field) => {
366  field.addEventListener("input", updatePreview);
367});
368
369updatePreview();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.