展示SEO信息
// ==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) => ({ "&": "&", "<": "<", ">": ">", '"': """, }[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(); }; })();