jskelet 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
1
  /**
2
- * Dev overlay: sağ altta yüzen baloncuk; hataları ve performans
3
- * istatistiklerini gösterir.
2
+ * Dev overlay: sağ altta yüzen baloncuk; hataları, performans
3
+ * istatistiklerini ve SEO taramasını gösterir.
4
4
  *
5
5
  * Bu dosya **build'e dahil değildir**. `npm run dev` sırasında sunucu onu
6
6
  * `/__jskelet/dev/overlay.js` altından ham olarak servis eder, layout da
7
- * script etiketini yalnızca development'ta basar. Bu yüzden burada bundler
8
- * yok: import yapmadan, tek dosya olarak çalışır.
7
+ * script etiketini yalnızca development'ta basar. Bundler yok: yalnızca aynı
8
+ * dizindeki `seo.js` gibi native ESM import'ları kullanılır.
9
9
  *
10
10
  * Tüm arayüz shadow DOM içinde durur; sayfanın CSS'i ile karışmaz.
11
11
  */
12
12
 
13
+ import { scanSeo, SeoHighlighter } from "./seo.js";
14
+
13
15
  const BASE = "/__jskelet/dev";
14
16
  /** Soket kurulamadığında düşülen yedek yoklama sıklığı. */
15
17
  const POLL_MS = 2000;
@@ -40,6 +42,19 @@ let open = false;
40
42
  /** Açık bırakılan yığın izleri; panel her poll'da yeniden çizildiği için gerekir. */
41
43
  let expanded = new Set();
42
44
 
45
+ /** Sayfa üzeri SEO dikdörtgenleri; panel kapalıyken de açık kalabilir. */
46
+ let seoHighlight = false;
47
+ /** Panel listesinde açılan sorun açıklaması. */
48
+ let seoExpandedId = null;
49
+ /** @type {import('./seo.js').SeoIssue[]} */
50
+ let seoIssues = [];
51
+ const seoLayer = new SeoHighlighter();
52
+ seoLayer.onSelect = (id) => {
53
+ seoExpandedId = id;
54
+ lastBody = "";
55
+ render();
56
+ };
57
+
43
58
  /**
44
59
  * Sunucu `node --watch` ile yeniden başladığında ya da sayfa tazelendiğinde
45
60
  * panelin kapanmaması için görünüm durumu ve tarayıcı hata günlüğü sekme
@@ -60,6 +75,8 @@ function saveState() {
60
75
  open,
61
76
  activeTab,
62
77
  aboutSection,
78
+ seoHighlight,
79
+ seoExpandedId,
63
80
  expanded: [...expanded],
64
81
  errors: clientErrors.slice(0, 30),
65
82
  }),
@@ -77,6 +94,8 @@ function loadState() {
77
94
  open = Boolean(saved.open);
78
95
  activeTab = saved.activeTab ?? "errors";
79
96
  aboutSection = saved.aboutSection ?? "framework";
97
+ seoHighlight = Boolean(saved.seoHighlight);
98
+ seoExpandedId = saved.seoExpandedId ?? null;
80
99
  expanded = new Set(saved.expanded ?? []);
81
100
  clientErrors.push(...(saved.errors ?? []));
82
101
  nextId = Math.max(nextId, ...clientErrors.map((item) => item.id + 1));
@@ -826,12 +845,43 @@ h4:first-child { margin-top: 0; }
826
845
  padding: 9px 11px; margin-bottom: 8px;
827
846
  }
828
847
  .item.warn { border-inline-start-color: var(--mid); }
848
+ .item.selected { border-color: rgba(255,255,255,.22); background: var(--surface-hover); }
829
849
  .item .meta { color: var(--muted); font-size: 10.5px; display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
830
850
  .tag { border: 1px solid var(--line); border-radius: 999px; padding: 0 7px; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; font-size: 9.5px; }
831
851
  .msg { word-break: break-word; color: var(--text); }
832
852
  .item pre { margin: 8px 0 0; padding: 8px 10px; background: rgba(0,0,0,.4); border-radius: 8px; white-space: pre-wrap; word-break: break-word; color: #b8c0cb; font-size: 11px; font-family: ui-monospace, SFMono-Regular, monospace; max-height: 150px; overflow: auto; }
853
+ .item .detail-text { margin: 8px 0 0; color: #b8c0cb; font-size: 12px; line-height: 1.55; }
833
854
  .link { background: none; border: 0; padding: 0; margin-top: 6px; color: var(--muted); font-size: 11px; font-weight: 600; cursor: pointer; }
834
855
  .link:hover { color: var(--text); }
856
+ .item.clickable { cursor: pointer; }
857
+ .item.clickable:hover { border-color: var(--line); }
858
+
859
+ .toggle-row {
860
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
861
+ margin: 12px 0 4px; padding: 10px 12px; border-radius: 12px;
862
+ background: var(--surface); border: 1px solid var(--line);
863
+ }
864
+ .toggle-row span { color: #c6ced9; font-size: 12px; }
865
+ .toggle {
866
+ position: relative; width: 36px; height: 20px; border-radius: 999px; border: 0;
867
+ background: rgba(255,255,255,.12); cursor: pointer; flex: none;
868
+ transition: background .15s ease;
869
+ }
870
+ .toggle.on { background: rgba(52,211,153,.45); }
871
+ .toggle i {
872
+ position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
873
+ background: #fff; transition: transform .15s ease;
874
+ }
875
+ .toggle.on i { transform: translateX(16px); }
876
+
877
+ .seo-chip {
878
+ display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
879
+ padding: 5px 11px; border-radius: 999px; border: 1px solid var(--line);
880
+ background: rgba(12,14,18,.92); color: #cfd6de; font-size: 11.5px; font-weight: 600;
881
+ box-shadow: 0 8px 24px rgba(0,0,0,.4); cursor: pointer;
882
+ }
883
+ .seo-chip.bad { color: #ffd7de; border-color: rgba(251,113,133,.4); }
884
+ .seo-chip.warn { color: #ffeec2; border-color: rgba(251,191,36,.4); }
835
885
 
836
886
  .copy {
837
887
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
@@ -891,6 +941,7 @@ let shadow = null;
891
941
  */
892
942
  const TAB_ICONS = {
893
943
  errors: `<svg viewBox="0 0 16 16"><path d="M8 5.5v4M8 11.5h.01M8 1.8 1.8 13.2h12.4L8 1.8Z"/></svg>`,
944
+ seo: `<svg viewBox="0 0 16 16"><circle cx="7.2" cy="7.2" r="4.4"/><path d="M10.4 10.4 13.6 13.6M5.2 7.2h4M7.2 5.2v4"/></svg>`,
894
945
  perf: `<svg viewBox="0 0 16 16"><path d="M2 13V8m4 5V3m4 10V6m4 7v-3"/></svg>`,
895
946
  server: `<svg viewBox="0 0 16 16"><path d="M2.5 3.5h11v3h-11zM2.5 9.5h11v3h-11zM5 5h.01M5 11h.01"/></svg>`,
896
947
  warm: `<svg viewBox="0 0 16 16"><path d="M8 14c2.5 0 4.2-1.7 4.2-3.9C12.2 6.5 8 2 8 2S3.8 6.5 3.8 10.1C3.8 12.3 5.5 14 8 14Z"/></svg>`,
@@ -916,6 +967,7 @@ const SKELETON = `
916
967
  </div>
917
968
  <div class="rail">
918
969
  <button class="tab" data-tab="errors">${TAB_ICONS.errors} Errors<span class="chip" data-part="chip" hidden></span></button>
970
+ <button class="tab" data-tab="seo">${TAB_ICONS.seo} SEO<span class="chip" data-part="seo-chip" hidden></span></button>
919
971
  <button class="tab" data-tab="perf">${TAB_ICONS.perf} Performance</button>
920
972
  <button class="tab" data-tab="server">${TAB_ICONS.server} Server<span class="chip neutral" data-part="version-chip" hidden>update</span></button>
921
973
  <button class="tab" data-tab="warm">${TAB_ICONS.warm} Prewarming<span class="chip neutral" data-part="warm-chip" hidden></span></button>
@@ -931,6 +983,7 @@ const SKELETON = `
931
983
  </div>
932
984
  </div>
933
985
  <div class="dock">
986
+ <button type="button" class="seo-chip" data-part="seo-dock" data-action="seo-focus" hidden></button>
934
987
  <span class="warm" data-part="warm" hidden>
935
988
  <span class="ring"></span>
936
989
  <span data-part="warm-text"></span>
@@ -1151,6 +1204,86 @@ function errorsTab() {
1151
1204
  `;
1152
1205
  }
1153
1206
 
1207
+ /**
1208
+ * DOM'u yeniden tarar; highlight açıksa katmanı da günceller.
1209
+ * @param {{ select?: string | null }} [opts]
1210
+ */
1211
+ function refreshSeo(opts = {}) {
1212
+ seoIssues = scanSeo(document);
1213
+ if (seoHighlight) {
1214
+ seoLayer.show(seoIssues, { select: opts.select ?? seoExpandedId });
1215
+ } else if (seoLayer.active) {
1216
+ seoLayer.hide();
1217
+ }
1218
+ }
1219
+
1220
+ function seoTab() {
1221
+ refreshSeo();
1222
+
1223
+ const errors = seoIssues.filter((item) => item.severity === "error").length;
1224
+ const warns = seoIssues.length - errors;
1225
+ const tone = errors ? "bad" : warns ? "mid" : "good";
1226
+
1227
+ clipboard.set(
1228
+ "seo",
1229
+ seoIssues
1230
+ .map(
1231
+ (item) =>
1232
+ `[${item.severity}] ${item.category} · ${item.title}\n${item.detail}`,
1233
+ )
1234
+ .join("\n\n") || "No SEO issues on this page.",
1235
+ );
1236
+
1237
+ const groups = new Map();
1238
+ for (const issue of seoIssues) {
1239
+ const list = groups.get(issue.category) ?? [];
1240
+ list.push(issue);
1241
+ groups.set(issue.category, list);
1242
+ }
1243
+
1244
+ const body = seoIssues.length
1245
+ ? [...groups.entries()]
1246
+ .map(
1247
+ ([category, list]) => `
1248
+ <h4>${escapeHtml(category)}</h4>
1249
+ ${list
1250
+ .map((issue) => {
1251
+ const openIssue = seoExpandedId === issue.id;
1252
+ return `<div class="item clickable ${issue.severity === "warn" ? "warn" : ""} ${openIssue ? "selected" : ""}"
1253
+ data-action="seo-issue" data-id="${escapeHtml(issue.id)}">
1254
+ <div class="meta">
1255
+ <span class="tag">${escapeHtml(issue.severity)}</span>
1256
+ <span class="spacer"></span>
1257
+ <span>${openIssue ? "▾ details" : "▸ details"}</span>
1258
+ </div>
1259
+ <div class="msg">${escapeHtml(issue.title)}</div>
1260
+ ${openIssue ? `<p class="detail-text">${escapeHtml(issue.detail)}</p>` : ""}
1261
+ </div>`;
1262
+ })
1263
+ .join("")}`,
1264
+ )
1265
+ .join("")
1266
+ : `<div class="empty">No SEO issues on this page.</div>`;
1267
+
1268
+ return `
1269
+ ${lede(
1270
+ tone,
1271
+ errors || warns
1272
+ ? `This page has <strong>${errors} SEO errors</strong> and <strong>${warns} warnings</strong>. Turn on page highlights to see red/yellow boxes on the offending elements; click a label for the full explanation.`
1273
+ : "Document title, description, headings, images and social tags look fine on this page.",
1274
+ )}
1275
+ <div class="toggle-row">
1276
+ <span>Highlight issues on the page</span>
1277
+ <button type="button" class="toggle ${seoHighlight ? "on" : ""}" data-action="seo-toggle"
1278
+ aria-pressed="${seoHighlight}" title="Draw boxes around SEO issues">
1279
+ <i></i>
1280
+ </button>
1281
+ </div>
1282
+ <h4>Findings<span class="head-action">${copyButton("seo", "copy all")}<button class="btn mini" data-action="seo-rescan" style="margin-inline-start:6px">${REFRESH_ICON} Rescan</button></span></h4>
1283
+ ${body}
1284
+ `;
1285
+ }
1286
+
1154
1287
  function perfTab() {
1155
1288
  const resources = resourceSummary();
1156
1289
  const islands = islandSummary();
@@ -1740,23 +1873,38 @@ function paint() {
1740
1873
  const warnCount =
1741
1874
  clientErrors.length + (serverStats?.errors ?? []).length - errorCount;
1742
1875
 
1743
- const tone = errorCount ? "bad" : warnCount ? "warn" : "";
1744
- const badgeCount = errorCount || warnCount;
1876
+ const seoErrors = seoIssues.filter((item) => item.severity === "error").length;
1877
+ const seoWarns = seoIssues.length - seoErrors;
1878
+
1879
+ const tone = errorCount || seoErrors ? "bad" : warnCount || seoWarns ? "warn" : "";
1880
+ const badgeCount = errorCount || warnCount || seoErrors || seoWarns;
1745
1881
  const lcpLabel = metrics.lcp ? `${Math.round(metrics.lcp)} ms` : "measuring";
1746
1882
 
1747
1883
  const bubble = root.querySelector(".bubble");
1748
1884
  bubble.className = `bubble ${tone}`;
1749
1885
  bubble.title = `JSkelet dev tools — LCP ${lcpLabel}${
1750
- badgeCount ? `, ${errorCount} errors, ${warnCount} warnings` : ""
1886
+ badgeCount
1887
+ ? `, ${errorCount} errors, ${warnCount} warnings, ${seoIssues.length} SEO`
1888
+ : ""
1751
1889
  }`;
1752
1890
 
1753
1891
  const badge = root.querySelector(".badge");
1754
1892
  badge.hidden = !badgeCount;
1755
- badge.className = `badge ${errorCount ? "" : "warn"}`;
1893
+ badge.className = `badge ${errorCount || seoErrors ? "" : "warn"}`;
1756
1894
  badge.textContent = badgeCount > 99 ? "99+" : String(badgeCount);
1757
1895
 
1758
1896
  paintPrewarm(root);
1759
1897
 
1898
+ const seoDock = root.querySelector("[data-part='seo-dock']");
1899
+ if (seoHighlight && seoIssues.length) {
1900
+ seoDock.hidden = false;
1901
+ seoDock.className = `seo-chip ${seoErrors ? "bad" : "warn"}`;
1902
+ seoDock.textContent = `${seoIssues.length} SEO`;
1903
+ seoDock.title = "Open SEO tab · page highlights are on";
1904
+ } else {
1905
+ seoDock.hidden = true;
1906
+ }
1907
+
1760
1908
  const backdrop = root.querySelector(".backdrop");
1761
1909
  backdrop.hidden = !open;
1762
1910
  if (!open) return;
@@ -1765,14 +1913,19 @@ function paint() {
1765
1913
  pill.className = `pill ${offline ? "warn" : tone}`;
1766
1914
  pill.querySelector("[data-part='pill']").textContent = offline
1767
1915
  ? "server restarting…"
1768
- : errorCount || warnCount
1769
- ? `${errorCount} errors · ${warnCount} warnings`
1916
+ : errorCount || warnCount || seoIssues.length
1917
+ ? `${errorCount} errors · ${warnCount} warnings · ${seoIssues.length} SEO`
1770
1918
  : "all good";
1771
1919
 
1772
1920
  const chip = root.querySelector("[data-part='chip']");
1773
1921
  chip.hidden = !errorCount;
1774
1922
  chip.textContent = String(errorCount);
1775
1923
 
1924
+ const seoChip = root.querySelector("[data-part='seo-chip']");
1925
+ seoChip.hidden = !seoIssues.length;
1926
+ seoChip.className = `chip ${seoErrors ? "" : "neutral"}`;
1927
+ seoChip.textContent = String(seoIssues.length);
1928
+
1776
1929
  const version = serverStats?.version;
1777
1930
  paintVersion(root, version);
1778
1931
  const versionChip = root.querySelector("[data-part='version-chip']");
@@ -1798,13 +1951,15 @@ function paint() {
1798
1951
  const html =
1799
1952
  activeTab === "errors"
1800
1953
  ? errorsTab()
1801
- : activeTab === "perf"
1802
- ? perfTab()
1803
- : activeTab === "about"
1804
- ? aboutTab()
1805
- : activeTab === "warm"
1806
- ? warmTab()
1807
- : serverTab();
1954
+ : activeTab === "seo"
1955
+ ? seoTab()
1956
+ : activeTab === "perf"
1957
+ ? perfTab()
1958
+ : activeTab === "about"
1959
+ ? aboutTab()
1960
+ : activeTab === "warm"
1961
+ ? warmTab()
1962
+ : serverTab();
1808
1963
 
1809
1964
  if (html !== lastBody) {
1810
1965
  const body = root.querySelector(".body");
@@ -1921,6 +2076,45 @@ function bind(root) {
1921
2076
  return;
1922
2077
  }
1923
2078
 
2079
+ if (target.dataset.action === "seo-toggle") {
2080
+ seoHighlight = !seoHighlight;
2081
+ if (!seoHighlight) seoExpandedId = null;
2082
+ refreshSeo();
2083
+ saveState();
2084
+ lastBody = "";
2085
+ render();
2086
+ return;
2087
+ }
2088
+
2089
+ if (target.dataset.action === "seo-rescan") {
2090
+ refreshSeo();
2091
+ lastBody = "";
2092
+ render();
2093
+ return;
2094
+ }
2095
+
2096
+ if (target.dataset.action === "seo-focus") {
2097
+ activeTab = "seo";
2098
+ setOpen(true);
2099
+ return;
2100
+ }
2101
+
2102
+ if (target.dataset.action === "seo-issue") {
2103
+ const id = target.dataset.id;
2104
+ seoExpandedId = seoExpandedId === id ? null : id;
2105
+ if (seoExpandedId) {
2106
+ seoHighlight = true;
2107
+ refreshSeo({ select: seoExpandedId });
2108
+ seoLayer.select(seoExpandedId);
2109
+ } else if (seoHighlight) {
2110
+ seoLayer.select(null);
2111
+ }
2112
+ saveState();
2113
+ lastBody = "";
2114
+ render();
2115
+ return;
2116
+ }
2117
+
1924
2118
  if (target.dataset.action === "copy") {
1925
2119
  copy(target.dataset.key);
1926
2120
  return;
@@ -1936,6 +2130,7 @@ function start() {
1936
2130
  captureMetrics();
1937
2131
  captureFetch();
1938
2132
  bind(ensureRoot());
2133
+ refreshSeo();
1939
2134
  render();
1940
2135
 
1941
2136
  // Panel kapalıyken de rozet güncel kalsın diye kanal her zaman açılır;
@@ -1949,13 +2144,33 @@ function start() {
1949
2144
  });
1950
2145
  window.addEventListener("pagehide", () => sendPageReport(true));
1951
2146
 
2147
+ // Island'lar ve lazy img'ler geç gelebilir; highlight açıksa bir kez daha tara.
2148
+ setTimeout(() => {
2149
+ if (!seoHighlight && activeTab !== "seo") return;
2150
+ refreshSeo();
2151
+ lastBody = "";
2152
+ render();
2153
+ }, 2500);
2154
+
1952
2155
  window.addEventListener("keydown", (event) => {
1953
2156
  if (event.altKey && event.code === "KeyD") {
1954
2157
  setOpen(!open);
1955
2158
  return;
1956
2159
  }
1957
2160
 
1958
- if (event.key === "Escape" && open) setOpen(false);
2161
+ if (event.key === "Escape") {
2162
+ if (open) {
2163
+ setOpen(false);
2164
+ return;
2165
+ }
2166
+ if (seoExpandedId) {
2167
+ seoExpandedId = null;
2168
+ if (seoHighlight) seoLayer.select(null);
2169
+ saveState();
2170
+ lastBody = "";
2171
+ render();
2172
+ }
2173
+ }
1959
2174
  });
1960
2175
  }
1961
2176