SEO

展示SEO信息

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         SEO
// @namespace    [email protected]
// @description  展示SEO信息
// @version      0.5
// @match        *://*.3d66.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

(() => {
  "use strict";

  const STORE_KEY = `__page_hud_clean__:${location.host}`;
  const HUD_STATE_KEY = "__seo_hud_state__";

  const getHudState = () => {
    try {
      if (typeof GM_getValue === "function") return GM_getValue(HUD_STATE_KEY, "");
    } catch {}
    try {
      return localStorage.getItem(HUD_STATE_KEY) || "";
    } catch {
      return "";
    }
  };

  const setHudState = (value) => {
    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(HUD_STATE_KEY, value);
        return;
      }
    } catch {}
    try {
      localStorage.setItem(HUD_STATE_KEY, value);
    } catch {}
  };

  const DEFAULT = {
    refreshMs: 1000,
    fields: [
      {
        label: "title",
        type: "headTagText",
        tagName: "title",
      },
      {
        label: "description",
        type: "meta",
        metaKey: "description",
      },
      {
        label: "keywords",
        type: "meta",
        metaKey: "keywords",
      },
      {
        label: "pubDate",
        type: "meta",
        metaKey: "pubDate",
      },
      {
        label: "upDate",
        type: "meta",
        metaKey: "upDate",
      },
      {
        label: "mobile-agent",
        type: "meta",
        metaKey: "mobile-agent",
      },
      {
        label: "canonical",
        type: "headLinkRelAttr",
        relKey: "canonical",
        attr: "href",
      },
      {
        label: 'link[rel="alternate"] attrs',
        type: "headLinkRelAttrs",
        relKey: "alternate",
      },
      {
        label: 'script[type="application/ld+json"]',
        type: "headScriptTypeTextList",
        scriptType: "application/ld+json",
      },
      {
        label: "H1 count",
        type: "js",
        expr: "(function(){ var hs = Array.prototype.slice.call(document.querySelectorAll('h1')); var texts = []; for (var i=0;i<hs.length;i++){ var t = (hs[i].textContent || '').trim(); if (t) texts.push(t); } return hs.length + '\\n' + texts.join('\\n'); })()",
      },
    ],
  };

  const load = () => {
    try {
      const raw = localStorage.getItem(STORE_KEY);
      if (!raw) return structuredClone(DEFAULT);
      const cfg = JSON.parse(raw);
      const ms = Number(cfg.refreshMs);
      return {
        refreshMs: Number.isFinite(ms) ? ms : DEFAULT.refreshMs,
        fields: Array.isArray(cfg.fields) ? cfg.fields : DEFAULT.fields,
        collapsed: !!cfg.collapsed,
      };
    } catch {
      return structuredClone(DEFAULT);
    }
  };

  const save = (cfg) => localStorage.setItem(STORE_KEY, JSON.stringify(cfg));

  const cfg = load();

  const host = document.createElement("div");
  host.style.cssText = "position:fixed;right:12px;bottom:12px;z-index:2147483647;";
  const shadow = host.attachShadow({ mode: "open" });

  const style = document.createElement("style");
  style.textContent = `
    *{box-sizing:border-box}
    .hud{
      width:500px;max-height:85vh;overflow:auto;border-radius:12px;
      background:rgba(20,20,20,.8);
      color:#fff;border:1px solid rgba(255,255,255,.14);
      font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
      box-shadow:0 12px 40px rgba(0,0,0,.35);
      scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.25) rgba(20,20,20,.92);
    }
    .hud::-webkit-scrollbar{
      width:10px;height:10px;
    }
    .hud::-webkit-scrollbar-track{
      background:transparent;
    }
    .hud::-webkit-scrollbar-thumb{
      background:rgba(255,255,255,.25);border-radius:8px;border:2px solid rgba(20,20,20,.92);
    }
    .hud::-webkit-scrollbar-thumb:hover{
      background:rgba(255,255,255,.35);
    }
    .hud::-webkit-scrollbar-corner{
      background:transparent;
    }
    .hdr{
      position:sticky;top:0;
      background:rgba(20,20,20,.75);
      z-index:1;
      padding:10px;border-bottom:1px solid rgba(255,255,255,.12);
      display:flex;gap:8px;align-items:center;justify-content:space-between;
      border-top-left-radius:12px;border-top-right-radius:12px;
    }
    .title{font-weight:700}
    .btns{display:flex;gap:6px;justify-content:flex-end}
    button{
      cursor:pointer;border-radius:10px;padding:5px 10px;
      border:1px solid rgba(255,255,255,.2);
      background:rgba(255,255,255,.10);color:#fff;
    }
    button:hover{background:rgba(255,255,255,.16)}
    .body{padding:10px}
    .row{padding:10px 0;border-bottom:1px solid rgba(255,255,255,.10)}
    .row:last-child{border-bottom:none}
    .topline{display:flex;gap:8px;align-items:center;justify-content:space-between}
    .lblwrap{display:flex;gap:8px;align-items:baseline;min-width:0}
    .lbl{
      font-weight:700;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
    .expr{
      opacity:.55;
      max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
    .val{margin-top:6px;opacity:.9;word-break:break-all;white-space:pre-wrap}
    .val.clamp{
      display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;
    }
    .val a{color:#9cd2ff;text-decoration:underline;word-break:break-all}
    .toggle{
      margin-top:6px;display:flex;justify-content:flex-end;gap:6px;
    }
    .toggle button{
      padding:3px 8px;border-radius:8px;font-size:11px;
    }
    .persistSel{
      background:rgba(255,229,100,.35);border-radius:3px;padding:0 1px;
    }
    .mini{
      display:flex;align-items:center;gap:8px;
      background:#141414;color:#fff;border:1px solid rgba(255,255,255,.14);
      border-radius:999px;padding:8px 10px;
      box-shadow:0 12px 40px rgba(0,0,0,.35);
    }
    .mini .pill{font-weight:700}
    .hint{opacity:.65}
  `;
  shadow.appendChild(style);

  const root = document.createElement("div");
  shadow.appendChild(root);

  const collapsedStateRaw = getHudState();
  const collapsedInitial = collapsedStateRaw ? collapsedStateRaw === "1" : !!cfg.collapsed;
  let expanded = !collapsedInitial;
  const expandedRows = new Set();
  let wheelBound = false;
  let refreshPaused = false;

  const esc = (s) =>
    (s ?? "").toString().replace(/[&<>"]/g, (c) => ({
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
    }[c]));

  const linkify = (s) => {
    const text = (s ?? "").toString();
    if (!text) return "";
    const re = /\bhttps?:\/\/[^\s"'<>]+/gi;
    let last = 0;
    let out = "";
    let match;
    while ((match = re.exec(text))) {
      const url = match[0];
      out += esc(text.slice(last, match.index));
      out += `<a href="${esc(url)}" target="_blank" rel="noreferrer">${esc(url)}</a>`;
      last = match.index + url.length;
    }
    out += esc(text.slice(last));
    return out;
  };

  const getHtmlValue = (f) => {
    try {
      const joinValues = (items) =>
        items
          .map((v) => (v == null ? "" : String(v)))
          .filter(Boolean)
          .join("\n");

      if (f.type === "js") {
        const expr = (f.expr || "").trim();
        if (!expr) return "";
        try {
          const result = Function(`"use strict"; return (${expr});`)();
          return result == null ? "" : String(result);
        } catch (err) {
          return `[JS error] ${err && err.message ? err.message : String(err)}`;
        }
      }

      if (f.type === "cssAttr") {
        const els = Array.from(document.querySelectorAll(f.selector || ""));
        const attr = (f.attr || "").trim();
        if (!attr) return "";
        return joinValues(els.map((el) => el.getAttribute(attr) || ""));
      }

      if (f.type === "cssText") {
        const els = Array.from(document.querySelectorAll(f.selector || ""));
        return joinValues(els.map((el) => el.outerHTML));
      }

      if (f.type === "meta") {
        const key = (f.metaKey || "").trim();
        if (!key) return "";
        const nameEls = Array.from(
          document.querySelectorAll(`meta[name="${CSS.escape(key)}"]`)
        );
        const propEls = Array.from(
          document.querySelectorAll(`meta[property="${CSS.escape(key)}"]`)
        );
        return joinValues(nameEls.concat(propEls).map((el) => el.outerHTML));
      }

      if (f.type === "headTagText") {
        const tagName = (f.tagName || "").trim();
        if (!tagName) return "";
        const target = tagName.toLowerCase();
        const els = Array.from(document.head?.children || []).filter(
          (node) => node.tagName?.toLowerCase() === target
        );
        return joinValues(els.map((el) => el.outerHTML));
      }

      if (f.type === "headLinkRelAttr") {
        const relKey = (f.relKey || "").trim();
        const attr = (f.attr || "").trim();
        if (!relKey || !attr) return "";
        const relNeedle = relKey.toLowerCase();

        return joinValues(
          Array.from(document.head?.querySelectorAll("link[rel]") || [])
            .filter((node) => {
              const rel = node.getAttribute("rel") || "";
              return rel
                .toLowerCase()
                .split(/\s+/)
                .filter(Boolean)
                .includes(relNeedle);
            })
            .map((el) => el.getAttribute(attr) || "")
        );
      }

      if (f.type === "headLinkRelAttrs") {
        const relKey = (f.relKey || "").trim();
        if (!relKey) return "";
        const relNeedle = relKey.toLowerCase();

        return joinValues(
          Array.from(document.head?.querySelectorAll("link[rel]") || [])
            .filter((node) => {
              const rel = node.getAttribute("rel") || "";
              return rel
                .toLowerCase()
                .split(/\s+/)
                .filter(Boolean)
                .includes(relNeedle);
            })
            .map((el) => el.outerHTML)
        );
      }

      if (f.type === "headScriptTypeTextList") {
        const scriptType = (f.scriptType || "").trim();
        if (!scriptType) return "";
        const typeNeedle = scriptType.toLowerCase();
        return joinValues(
          Array.from(document.head?.querySelectorAll("script[type]") || [])
            .filter((el) => (el.getAttribute("type") || "").toLowerCase() === typeNeedle)
            .map((el) => el.outerHTML)
        );
      }

      return "";
    } catch {
      return "";
    }
  };

  const NOFOLLOW_STYLE_ID = "__hud_nofollow_style__";
  const NOFOLLOW_MARK_CLASS = "__hud-nofollow";
  const NOFOLLOW_BADGE_CLASS = "__hud-nofollow-badge";

  const ensureNofollowStyle = () => {
    if (document.getElementById(NOFOLLOW_STYLE_ID)) return;
    const styleEl = document.createElement("style");
    styleEl.id = NOFOLLOW_STYLE_ID;
    styleEl.textContent = `
      a.${NOFOLLOW_MARK_CLASS} .${NOFOLLOW_BADGE_CLASS}{
        position:absolute !important;top:-6px !important;right:-6px !important;z-index:99999 !important;
        pointer-events:none !important;
        display:inline-flex !important;align-items:center !important;
        padding:2px 5px !important;
        font-size:9px !important;line-height:1 !important;border-radius:6px !important;
        border:1px solid rgba(255,255,255,.35) !important;
        color:rgba(255,255,255,.95) !important;
        background:rgba(200,50,50,.85) !important;
        white-space:nowrap !important;
        transform:scale(0.9) !important;
      }
    `;
    document.head.appendChild(styleEl);
  };

  const applyNofollowBadges = () => {
    ensureNofollowStyle();
    document.querySelectorAll("a[rel]").forEach((a) => {
      const rel = (a.getAttribute("rel") || "")
        .toLowerCase()
        .split(/\s+/)
        .filter(Boolean);
      const hasNofollow = rel.includes("nofollow");
      const badge = a.querySelector(`.${NOFOLLOW_BADGE_CLASS}`);

      if (!hasNofollow) {
        a.classList.remove(NOFOLLOW_MARK_CLASS);
        badge?.remove();
        return;
      }

      a.classList.add(NOFOLLOW_MARK_CLASS);

      const computed = getComputedStyle(a);
      if (computed.position === "static") {
        a.style.position = "relative";
      }

      if (!badge) {
        const nextBadge = document.createElement("span");
        nextBadge.className = NOFOLLOW_BADGE_CLASS;
        nextBadge.textContent = "✖";
        nextBadge.style.cssText =
          "position:absolute;top:-6px;right:-6px;z-index:99999;pointer-events:none;display:inline-flex;align-items:center;padding:2px 5px;font-size:9px;line-height:1;border-radius:6px;border:1px solid rgba(255,255,255,.35);color:rgba(255,255,255,.95);background:rgba(200,50,50,.85);white-space:nowrap;transform:scale(0.9);";
        a.appendChild(nextBadge);
      }
    });
  };

  const toggle = () => {
    expanded = !expanded;
    cfg.collapsed = !expanded;
    save(cfg);
    setHudState(expanded ? "0" : "1");
    render();
  };

  const bindWheel = () => {
    if (wheelBound) return;
    host.addEventListener(
      "wheel",
      (e) => {
        if (!expanded) return;
        const hudEl = root.querySelector(".hud");
        if (!hudEl) return;

        const rect = hudEl.getBoundingClientRect();
        const inHud =
          e.clientX >= rect.left &&
          e.clientX <= rect.right &&
          e.clientY >= rect.top &&
          e.clientY <= rect.bottom;

        if (inHud) {
          hudEl.scrollTop += e.deltaY;
          e.preventDefault();
        }
      },
      { passive: false }
    );
    wheelBound = true;
  };

  const hasActiveSelectionInHud = () => {
    if (shadow.getSelection) {
      const s = shadow.getSelection();
      if (s && s.rangeCount > 0 && !s.isCollapsed) return true;
    }

    const sel = document.getSelection();
    if (!sel || sel.isCollapsed) return false;

    if (sel.anchorNode === host || sel.focusNode === host) return true;

    const a = sel.anchorNode;
    const f = sel.focusNode;
    return (a && root.contains(a)) || (f && root.contains(f));
  };

  const render = () => {
    if (hasActiveSelectionInHud() && !window.__HUD_MANUAL_REFRESH__) {
      refreshPaused = true;
      return;
    }

    if (refreshPaused && !window.__HUD_MANUAL_REFRESH__) return;

    if (!expanded) {
      root.innerHTML = `
        <div class="mini">
          <div class="pill">SEO</div>
          <button id="__show">Show</button>
        </div>
      `;
      root.querySelector("#__show")?.addEventListener("click", () => toggle());
      applyNofollowBadges();
      return;
    }

    const prevScrollTop = root.querySelector(".hud")?.scrollTop ?? 0;

    const rows = cfg.fields
      .map((f) => {
        const rawHtml = getHtmlValue(f) ?? "";
        const lineCount = rawHtml ? rawHtml.split(/\r?\n/).length : 0;
        const isLongText = rawHtml.length > 200 || lineCount > 3;
        const rowKey = `${f.type}::${f.label || ""}`;
        const isExpanded = !isLongText || expandedRows.has(rowKey);
        const valHTML = linkify(rawHtml);
        const valTitle = isLongText ? esc(rawHtml) : "";
        const valClass = isLongText && !isExpanded ? "val clamp" : "val";
        const expr = f.type === "js" ? (f.expr || "") : "";

        return `
          <div class="row" data-row-key="${esc(rowKey)}">
            <div class="topline">
              <div class="lblwrap" title="${esc(f.label || "")}">
                <div class="lbl">${esc(f.label || "Field")}</div>
                ${expr ? `<div class="expr" title="${esc(expr)}">${esc(expr)}</div>` : ""}
              </div>
            </div>
            <div class="${valClass}"${valTitle ? ` title="${valTitle}"` : ""}>${
              valHTML || '<span style="opacity:.55">—</span>'
            }</div>
            ${
              isLongText
                ? `<div class="toggle"><button data-toggle="${esc(rowKey)}">${
                    isExpanded ? "收起" : "展开"
                  }</button></div>`
                : ""
            }
          </div>
        `;
      })
      .join("");

    root.innerHTML = `
      <div class="hud">
        <div class="hdr">
          <div>
            <span class="title">查看SEO数据</span>
            <span style="opacity:.65;margin-left:8px">${esc(location.host)}</span>
          </div>
          <div class="btns">
            <button id="__hide">Hide</button>
          </div>
        </div>

        <div class="body">
          ${rows || `<div style="opacity:.75">No fields. Edit DEFAULT.fields in the script.</div>`}
        </div>
      </div>
    `;

    const hudEl = root.querySelector(".hud");
    if (hudEl) hudEl.scrollTop = prevScrollTop;

    root.querySelector("#__hide")?.addEventListener("click", () => toggle());

    root.querySelectorAll("[data-toggle]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const key = btn.getAttribute("data-toggle") || "";
        if (!key) return;
        if (expandedRows.has(key)) expandedRows.delete(key);
        else expandedRows.add(key);
        render();
      });
    });

    applyNofollowBadges();
  };

  window.addEventListener("keydown", (e) => {
    if (e.ctrlKey && e.shiftKey && (e.key === "H" || e.key === "h")) toggle();
  });

  document.documentElement.appendChild(host);
  bindWheel();

  root.addEventListener("mousedown", () => {
    refreshPaused = true;
  });

  document.addEventListener("selectionchange", () => {
    refreshPaused = hasActiveSelectionInHud();
  });

  document.addEventListener("mouseup", () => {
    setTimeout(() => {
      refreshPaused = hasActiveSelectionInHud();
      if (!refreshPaused) render();
    }, 10);
  });

  render();

  let timer = null;
  const startTimer = () => {
    if (timer) clearInterval(timer);
    if (Number(cfg.refreshMs) > 0) {
      timer = setInterval(() => {
        if (!refreshPaused) render();
      }, Number(cfg.refreshMs));
    } else {
      timer = null;
    }
  };

  startTimer();

  window.__HUD_CFG__ = cfg;
  window.__HUD_SAVE__ = () => {
    save(cfg);
    startTimer();
    render();
  };
})();