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.
- package/CHANGELOG.md +14 -0
- package/docs/02-mimari.md +2 -0
- package/docs/04-render-ve-sablonlar.md +9 -5
- package/docs/07-yapilandirma.md +36 -9
- package/docs/08-build.md +7 -0
- package/docs/09-dev-araclari.md +12 -3
- package/docs/10-dagitim.md +5 -2
- package/docs/11-tasima.md +4 -3
- package/docs/README.md +1 -1
- package/docs/en/02-architecture.md +2 -0
- package/docs/en/04-rendering.md +9 -5
- package/docs/en/07-configuration.md +37 -9
- package/docs/en/08-build.md +7 -0
- package/docs/en/09-dev-tools.md +13 -3
- package/docs/en/10-deployment.md +6 -3
- package/docs/en/11-migration.md +4 -3
- package/docs/en/README.md +1 -1
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +233 -18
- package/src/client/devtools/seo.js +628 -0
- package/src/config/defaults.js +26 -0
- package/src/config/index.js +99 -2
- package/src/index.js +1 -0
- package/src/server/create-app.js +9 -0
- package/src/server/dev/devtools.js +7 -0
- package/src/server/image-optimizer.js +338 -0
- package/src/views/helpers/tags.js +52 -5
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev overlay: sağ altta yüzen baloncuk;
|
|
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.
|
|
8
|
-
*
|
|
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
|
|
1744
|
-
const
|
|
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
|
|
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 === "
|
|
1802
|
-
?
|
|
1803
|
-
: activeTab === "
|
|
1804
|
-
?
|
|
1805
|
-
: activeTab === "
|
|
1806
|
-
?
|
|
1807
|
-
:
|
|
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"
|
|
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
|
|