KEX Page Checker

Scans KEX SOP pages for post-publication issues (broken links, missing images, leftover Watson refs, encoding problems)

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         KEX Page Checker
// @namespace    https://knowledge.sps.amazon.dev
// @version      1.0.0
// @description  Scans KEX SOP pages for post-publication issues (broken links, missing images, leftover Watson refs, encoding problems)
// @match        https://knowledge.sps.amazon.dev/k?id=*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  // Wait for page content to load
  function waitForContent() {
    var check = function () {
      if (document.querySelector(".WatsonSOPBody") || document.querySelector("h1") || document.querySelector("h2")) {
        setTimeout(createUI, 1500); // extra delay for images to load
      } else {
        setTimeout(check, 1000);
      }
    };
    check();
  }

  function createUI() {
    // Create floating button
    var btn = document.createElement("button");
    btn.id = "kex-checker-btn";
    btn.textContent = "Check Page";
    btn.style.cssText = "position:fixed;bottom:20px;right:20px;z-index:999998;background:#232F3E;color:#FF6200;border:none;border-radius:8px;padding:10px 18px;font-size:14px;font-weight:bold;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,0.3);font-family:Arial,sans-serif;";
    btn.addEventListener("click", runChecker);
    document.body.appendChild(btn);
  }

  function runChecker() {
    var results = [];
    var highlights = [];

    // Clean up previous run
    var oldStyle = document.getElementById("kex-checker-style");
    if (oldStyle) oldStyle.remove();
    var oldPanel = document.getElementById("kex-checker-panel");
    if (oldPanel) oldPanel.remove();
    document.querySelectorAll(".kex-check-highlight").forEach(function (el) {
      el.classList.remove("kex-check-highlight");
      el.removeAttribute("data-kex-issue");
    });

    // Inject styles
    var style = document.createElement("style");
    style.id = "kex-checker-style";
    style.textContent = [
      ".kex-check-highlight{outline:3px solid red !important;outline-offset:2px;position:relative;}",
      ".kex-check-highlight::after{content:attr(data-kex-issue);position:absolute;top:-20px;left:0;background:red;color:#fff;font-size:11px;padding:2px 6px;border-radius:3px;white-space:nowrap;z-index:99999;font-family:Arial,sans-serif;}",
      "#kex-checker-panel{position:fixed;top:10px;right:10px;width:440px;max-height:80vh;overflow-y:auto;background:#fff;border:2px solid #232F3E;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.3);z-index:999999;font-family:Arial,sans-serif;font-size:13px;}",
      "#kex-checker-panel .chk-header{background:#232F3E;color:#fff;padding:12px 16px;border-radius:8px 8px 0 0;display:flex;justify-content:space-between;align-items:center;}",
      "#kex-checker-panel .chk-header h3{margin:0;font-size:15px;color:#FF6200;}",
      "#kex-checker-panel .close-btn{cursor:pointer;font-size:18px;color:#fff;font-weight:bold;}",
      "#kex-checker-panel .chk-body{padding:12px 16px;}",
      "#kex-checker-panel .section{margin-bottom:12px;}",
      "#kex-checker-panel .section h4{margin:0 0 6px;font-size:13px;color:#232F3E;}",
      "#kex-checker-panel .pass{color:#27ae60;font-weight:bold;}",
      "#kex-checker-panel .fail{color:#c0392b;font-weight:bold;}",
      "#kex-checker-panel .issue{padding:4px 0;border-bottom:1px solid #f0f0f0;cursor:pointer;font-size:12px;}",
      "#kex-checker-panel .issue:hover{background:#f8f8f8;}",
      "#kex-checker-panel .summary{padding:10px;background:#f8f9fa;border-radius:6px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;}",
      "#kex-checker-panel .export-btn{padding:6px 14px;background:#232F3E;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:bold;cursor:pointer;font-family:Arial,sans-serif;}",
      "#kex-checker-panel .export-btn:hover{background:#37475a;}"
    ].join("");
    document.head.appendChild(style);

    function flag(el, msg, cat) {
      if (el && el.classList) {
        el.classList.add("kex-check-highlight");
        el.setAttribute("data-kex-issue", msg);
      }
      highlights.push(el);
      results.push({
        cat: cat,
        msg: msg,
        el: el,
        detail: el ? (el.getAttribute("href") || el.getAttribute("src") || el.tagName) : "n/a"
      });
    }

    // ── LINKS ──
    var links = document.querySelectorAll("a[href]");
    links.forEach(function (a) {
      var href = a.getAttribute("href") || "";
      var text = (a.textContent || "").trim();

      if (href.indexOf("share.amazon.com/sites/amazonwatson") !== -1) {
        flag(a, "Watson link still present", "links");
      }
      if (href.indexOf("REPLACE_WITH_GUID") !== -1) {
        flag(a, "GUID placeholder not replaced", "links");
      }
      if (href.match(/[?&]id=(#|$|&)/) || href.match(/[?&]id=$/)) {
        flag(a, "Paragon link with empty GUID", "links");
      }
      if (!text && !a.querySelector("img")) {
        flag(a, "Empty link (no text or image)", "links");
      }
      if (href === "about:blank" || href === "javascript:void(0)" || href === "javascript:void") {
        flag(a, "Dead link (about:blank/void)", "links");
      }
      if (href.startsWith("#") && href.length > 1) {
        var anchor = href.substring(1);
        if (anchor.startsWith("help-page-header-")) return;
        if (!document.getElementById(anchor) && !document.querySelector('[name="' + CSS.escape(anchor) + '"]')) {
          flag(a, "Broken anchor: " + href, "links");
        }
      }
    });

    // ── IMAGES ──
    var imgs = document.querySelectorAll("img");
    imgs.forEach(function (img) {
      if (img.complete && img.naturalWidth === 0 && img.src && img.src.indexOf("data:") !== 0) {
        flag(img, "Broken image (failed to load)", "images");
      }
      var src = img.getAttribute("src") || "";
      if (src.indexOf("Resources/Images/") !== -1 || src.indexOf("Resources\\Images\\") !== -1) {
        flag(img, "Local image path (not Media Central)", "images");
      }
      if (!img.hasAttribute("alt")) {
        flag(img, "Missing alt attribute", "images");
      }
    });

    // ── CSS / JS ──
    var inlineStyles = document.querySelectorAll("style");
    var leftoverLinks = document.querySelectorAll('link[rel="stylesheet"]');

    if (inlineStyles.length > 0) {
      results.push({ cat: "css_js", msg: inlineStyles.length + ' inline <style> block(s) found (old embed approach?)', el: null, detail: "" });
    }
    if (leftoverLinks.length > 0) {
      leftoverLinks.forEach(function (l) {
        flag(l, "Leftover <link stylesheet> tag", "css_js");
      });
    }

    // ── CONTENT QUALITY ──
    var allText = document.body.innerText || "";
    var mojibakePatterns = [/Ã[\x80-\xBF]/g, /Â[\x80-\xBF]/g, /â€[^\s]/g, /é/g, /ö/g, /ü/g];
    var hasMojibake = false;
    mojibakePatterns.forEach(function (p) { if (p.test(allText)) hasMojibake = true; });
    if (hasMojibake) {
      results.push({ cat: "content", msg: "Possible encoding issues (mojibake characters detected)", el: null, detail: "" });
    }

    var watsonSource = document.getElementById("WatsonSOPSource");
    if (watsonSource && watsonSource.offsetHeight > 0) {
      flag(watsonSource, "WatsonSOPSource element visible", "content");
    }

    var siteBanner = document.getElementById("SiteMovedBanner_MessageBar_Site");
    if (siteBanner && siteBanner.offsetHeight > 0) {
      flag(siteBanner, "SiteMovedBanner still visible", "content");
    }

    var madcapConds = document.querySelectorAll("[MadCap\\:conditions]");
    if (madcapConds.length > 0) {
      madcapConds.forEach(function (el) {
        flag(el, "MadCap:conditions attribute in rendered HTML", "content");
      });
    }

    var allIds = document.querySelectorAll("[id]");
    var idMap = {};
    allIds.forEach(function (el) {
      var id = el.id;
      if (id) {
        if (idMap[id]) { flag(el, "Duplicate ID: " + id, "content"); }
        else { idMap[id] = true; }
      }
    });

    var emptyCollapsibles = document.querySelectorAll(".collapsible");
    emptyCollapsibles.forEach(function (btn) {
      var next = btn.nextElementSibling;
      if (next && next.classList.contains("collapsed") && (next.textContent || "").trim().length === 0) {
        flag(btn, "Empty collapsible section", "content");
      }
    });

    // ── LAYOUT ──
    var bodyWidth = document.body.scrollWidth;
    var viewWidth = window.innerWidth;
    if (bodyWidth > viewWidth + 5) {
      results.push({ cat: "layout", msg: "Content wider than viewport (horizontal scroll: " + bodyWidth + "px vs " + viewWidth + "px)", el: null, detail: "" });
    }
    imgs.forEach(function (img) {
      if (img.naturalWidth > 0 && img.offsetWidth > img.parentElement.offsetWidth + 10) {
        flag(img, "Image exceeds container width", "layout");
      }
    });

    // ── BUILD PANEL ──
    var cats = { links: "Links", images: "Images", css_js: "CSS / JS", content: "Content Quality", layout: "Layout" };
    var catResults = {};
    results.forEach(function (r) {
      if (!catResults[r.cat]) catResults[r.cat] = [];
      catResults[r.cat].push(r);
    });
    var totalIssues = results.length;

    var panel = document.createElement("div");
    panel.id = "kex-checker-panel";

    var html = '<div class="chk-header"><h3>KEX Page Checker</h3><span class="close-btn" id="kex-checker-close">X</span></div>';
    html += '<div class="chk-body">';
    html += '<div class="summary">';
    if (totalIssues === 0) {
      html += '<span class="pass">All checks passed! No issues found.</span>';
    } else {
      html += '<span class="fail">' + totalIssues + ' issue' + (totalIssues > 1 ? 's' : '') + ' found</span>';
    }
    html += '</div>';
    html += '<button class="export-btn" id="kex-export-csv">Export Issues as CSV</button>';

    Object.keys(cats).forEach(function (catKey) {
      var catName = cats[catKey];
      var items = catResults[catKey] || [];
      html += '<div class="section"><h4>' + catName + ' ';
      if (items.length === 0) {
        html += '<span class="pass">PASS</span>';
      } else {
        html += '<span class="fail">(' + items.length + ')</span>';
      }
      html += '</h4>';
      items.forEach(function (item, idx) {
        var elIdx = highlights.indexOf(item.el);
        html += '<div class="issue" data-highlight-idx="' + elIdx + '">' + item.msg + '</div>';
      });
      html += '</div>';
    });

    html += '</div>';

    panel.innerHTML = html;
    document.body.appendChild(panel);

    // ── Event: close panel ──
    document.getElementById("kex-checker-close").addEventListener("click", function () {
      document.getElementById("kex-checker-panel").remove();
      document.querySelectorAll(".kex-check-highlight").forEach(function (e) {
        e.classList.remove("kex-check-highlight");
        e.removeAttribute("data-kex-issue");
      });
    });

    // ── Event: click issue to scroll ──
    panel.querySelectorAll(".issue").forEach(function (issueEl) {
      issueEl.addEventListener("click", function () {
        var idx = parseInt(this.getAttribute("data-highlight-idx"));
        if (highlights[idx]) {
          highlights[idx].scrollIntoView({ behavior: "smooth", block: "center" });
        }
      });
    });

    // ── Event: export CSV ──
    document.getElementById("kex-export-csv").addEventListener("click", function () {
      var csv = "Category,Issue,Detail,Page URL\n";
      results.forEach(function (r) {
        var cat = cats[r.cat] || r.cat;
        var msg = r.msg.replace(/"/g, '""');
        var detail = (r.detail || "").replace(/"/g, '""');
        csv += '"' + cat + '","' + msg + '","' + detail + '","' + window.location.href + '"\n';
      });
      var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
      var url = URL.createObjectURL(blob);
      var a = document.createElement("a");
      a.href = url;
      a.download = "kex_page_check_" + document.title.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 40) + ".csv";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    });
  }

  waitForContent();
})();