Add full-feature inline fallback when script.js fails
If external JS fails to load on some static hosts, run an inline version supporting add/edit/delete/favorite/search/sort/import/export/theme so the UI is never dead. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
441
index.html
441
index.html
@@ -341,59 +341,446 @@
|
|||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Fallback: if script.js fails to load, render basic cards -->
|
<!-- Fallback: if script.js fails, run full-feature inline app -->
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => {
|
(function () {
|
||||||
if (window.__LINKS_APP_BOOTED__) return;
|
function safeJsonParse(s, fallback) {
|
||||||
const grid = document.getElementById("grid");
|
|
||||||
const empty = document.getElementById("empty");
|
|
||||||
const dataEl = document.getElementById("linksData");
|
|
||||||
if (!grid || !dataEl) return;
|
|
||||||
try {
|
try {
|
||||||
const links = JSON.parse(dataEl.textContent || "[]");
|
return JSON.parse(s);
|
||||||
if (!Array.isArray(links) || links.length === 0) return;
|
} catch {
|
||||||
const esc = (s) =>
|
return fallback;
|
||||||
String(s)
|
}
|
||||||
|
}
|
||||||
|
function esc(s) {
|
||||||
|
return String(s)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
.replaceAll("<", "<")
|
.replaceAll("<", "<")
|
||||||
.replaceAll(">", ">")
|
.replaceAll(">", ">")
|
||||||
.replaceAll('"', """)
|
.replaceAll('"', """)
|
||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
const domain = (u) => {
|
}
|
||||||
|
function normalizeUrl(url) {
|
||||||
|
const u = String(url || "").trim();
|
||||||
|
if (!u) return "";
|
||||||
|
if (/^https?:\/\//i.test(u)) return u;
|
||||||
|
return "https://" + u;
|
||||||
|
}
|
||||||
|
function domain(u) {
|
||||||
try {
|
try {
|
||||||
return new URL(u).host;
|
return new URL(u).host;
|
||||||
} catch {
|
} catch {
|
||||||
return String(u || "").replace(/^https?:\/\//i, "").split("/")[0] || "";
|
return String(u || "").replace(/^https?:\/\//i, "").split("/")[0] || "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function nowIso() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a tick for external script.js to possibly boot.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.__LINKS_APP_BOOTED__) return;
|
||||||
|
window.__LINKS_APP_BOOTED__ = true;
|
||||||
|
|
||||||
|
const STORAGE_KEY = "links_home_v1";
|
||||||
|
const THEME_KEY = "links_home_theme_v1";
|
||||||
|
|
||||||
|
const el = {
|
||||||
|
q: document.getElementById("q"),
|
||||||
|
sort: document.getElementById("sort"),
|
||||||
|
onlyFav: document.getElementById("onlyFav"),
|
||||||
|
meta: document.getElementById("meta"),
|
||||||
|
grid: document.getElementById("grid"),
|
||||||
|
empty: document.getElementById("empty"),
|
||||||
|
btnAdd: document.getElementById("btnAdd"),
|
||||||
|
btnImport: document.getElementById("btnImport"),
|
||||||
|
btnExport: document.getElementById("btnExport"),
|
||||||
|
btnTheme: document.getElementById("btnTheme"),
|
||||||
|
toast: document.getElementById("toast"),
|
||||||
|
modal: document.getElementById("modal"),
|
||||||
|
btnClose: document.getElementById("btnClose"),
|
||||||
|
btnCancel: document.getElementById("btnCancel"),
|
||||||
|
form: document.getElementById("form"),
|
||||||
|
id: document.getElementById("id"),
|
||||||
|
title: document.getElementById("title"),
|
||||||
|
url: document.getElementById("url"),
|
||||||
|
description: document.getElementById("description"),
|
||||||
|
tags: document.getElementById("tags"),
|
||||||
|
favorite: document.getElementById("favorite"),
|
||||||
|
file: document.getElementById("file"),
|
||||||
};
|
};
|
||||||
grid.innerHTML = links
|
|
||||||
.map((l) => {
|
function toast(msg) {
|
||||||
const t = esc(l.title || domain(l.url) || "Link");
|
if (!el.toast) return;
|
||||||
const d = esc(domain(l.url));
|
el.toast.textContent = msg;
|
||||||
const u = esc(l.url || "#");
|
el.toast.hidden = false;
|
||||||
const desc = esc(l.description || "");
|
clearTimeout(toast._t);
|
||||||
|
toast._t = setTimeout(() => (el.toast.hidden = true), 2200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStore() {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
const data = raw ? safeJsonParse(raw, null) : null;
|
||||||
|
const store = { overridesById: {}, tombstones: [], custom: [] };
|
||||||
|
if (!data || typeof data !== "object") return store;
|
||||||
|
if (data.overridesById && typeof data.overridesById === "object") store.overridesById = data.overridesById;
|
||||||
|
if (Array.isArray(data.tombstones)) store.tombstones = data.tombstones;
|
||||||
|
if (Array.isArray(data.custom)) store.custom = data.custom;
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
function saveStore() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.store));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTags(tagsText) {
|
||||||
|
if (!tagsText) return [];
|
||||||
|
return String(tagsText)
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLink(link) {
|
||||||
|
const url = normalizeUrl(link.url);
|
||||||
|
const id = String(link.id || "").trim() || "link-" + Math.random().toString(16).slice(2);
|
||||||
|
const title = String(link.title || "").trim() || domain(url) || "Link";
|
||||||
|
const description = String(link.description || "").trim();
|
||||||
|
const tags = Array.isArray(link.tags) ? link.tags.map((t) => String(t).trim()).filter(Boolean) : [];
|
||||||
|
const favorite = Boolean(link.favorite);
|
||||||
|
const createdAt = String(link.createdAt || nowIso());
|
||||||
|
const updatedAt = String(link.updatedAt || createdAt);
|
||||||
|
return { id, title, url, description, tags, favorite, createdAt, updatedAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseLinks = (() => {
|
||||||
|
const dataEl = document.getElementById("linksData");
|
||||||
|
const parsed = dataEl ? safeJsonParse(dataEl.textContent || "[]", []) : [];
|
||||||
|
return Array.isArray(parsed) ? parsed.map(normalizeLink) : [];
|
||||||
|
})();
|
||||||
|
const baseOrder = new Map(baseLinks.map((l, i) => [l.id, i]));
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
store: loadStore(),
|
||||||
|
query: "",
|
||||||
|
sortKey: (el.sort && el.sort.value) || "json",
|
||||||
|
onlyFav: Boolean(el.onlyFav && el.onlyFav.checked),
|
||||||
|
};
|
||||||
|
|
||||||
|
function toTime(s) {
|
||||||
|
const t = Date.parse(String(s || ""));
|
||||||
|
return Number.isFinite(t) ? t : 0;
|
||||||
|
}
|
||||||
|
function orderKey(link) {
|
||||||
|
const idx = baseOrder.get(link.id);
|
||||||
|
if (typeof idx === "number") return idx;
|
||||||
|
return 1_000_000 + toTime(link.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergedLinks() {
|
||||||
|
const tomb = new Set(state.store.tombstones || []);
|
||||||
|
const overrides = state.store.overridesById || {};
|
||||||
|
const byId = new Map();
|
||||||
|
for (const base of baseLinks) {
|
||||||
|
if (!base || !base.id) continue;
|
||||||
|
if (tomb.has(base.id)) continue;
|
||||||
|
const o = overrides[base.id];
|
||||||
|
byId.set(base.id, Object.assign({}, base, o || {}));
|
||||||
|
}
|
||||||
|
for (const c of state.store.custom || []) {
|
||||||
|
const n = normalizeLink(c);
|
||||||
|
byId.set(n.id, n);
|
||||||
|
}
|
||||||
|
return Array.from(byId.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a, b) {
|
||||||
|
const key = state.sortKey || "json";
|
||||||
|
if (key === "json") return orderKey(a) - orderKey(b);
|
||||||
|
if (key === "favorite") {
|
||||||
|
if (a.favorite !== b.favorite) return a.favorite ? -1 : 1;
|
||||||
|
return orderKey(a) - orderKey(b);
|
||||||
|
}
|
||||||
|
if (key === "name") return a.title.localeCompare(b.title, "ko");
|
||||||
|
if (key === "domain") return domain(a.url).localeCompare(domain(b.url), "en");
|
||||||
|
return String(b.updatedAt).localeCompare(String(a.updatedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(link, q) {
|
||||||
|
if (!q) return true;
|
||||||
|
const hay = [link.title, link.url, domain(link.url), link.description || "", (link.tags || []).join(" ")]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardHtml(link) {
|
||||||
|
const d = esc(domain(link.url));
|
||||||
|
const t = esc(link.title);
|
||||||
|
const u = esc(link.url);
|
||||||
|
const desc = esc(link.description || "");
|
||||||
|
const star = link.favorite ? "star on" : "star";
|
||||||
|
const tags = (link.tags || []).slice(0, 8).map((x) => `<span class="tag">#${esc(x)}</span>`).join("");
|
||||||
|
const favTag = link.favorite ? `<span class="tag fav">★ 즐겨찾기</span>` : "";
|
||||||
|
const letter = esc((link.title || d || "L").trim().slice(0, 1).toUpperCase());
|
||||||
return `
|
return `
|
||||||
<article class="card">
|
<article class="card" data-id="${esc(link.id)}">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<div class="favicon" aria-hidden="true"><div class="letter">${t.slice(0, 1)}</div></div>
|
<div class="favicon" aria-hidden="true"><div class="letter">${letter}</div></div>
|
||||||
<div class="title-wrap">
|
<div class="title-wrap">
|
||||||
<div class="title">${t}</div>
|
<div class="title" title="${t}">${t}</div>
|
||||||
<div class="domain">${d}</div>
|
<div class="domain" title="${d}">${d}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="icon-btn" type="button" data-act="fav" title="즐겨찾기">
|
||||||
|
<span class="${star}" aria-hidden="true">★</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-desc">${desc || " "}</div>
|
<div class="card-desc">${desc || " "}</div>
|
||||||
|
<div class="tags">${favTag}${tags}</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a class="btn mini" href="${u}" target="_blank" rel="noopener noreferrer">열기</a>
|
<a class="btn mini" href="${u}" target="_blank" rel="noopener noreferrer">열기</a>
|
||||||
|
<button class="btn mini" type="button" data-act="copy">URL 복사</button>
|
||||||
|
<button class="btn mini" type="button" data-act="edit">편집</button>
|
||||||
|
<button class="btn mini mini-danger" type="button" data-act="del">삭제</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`;
|
`;
|
||||||
})
|
}
|
||||||
.join("");
|
|
||||||
if (empty) empty.hidden = true;
|
function render() {
|
||||||
} catch {}
|
const q = (state.query || "").trim().toLowerCase();
|
||||||
}, 80);
|
const all = getMergedLinks();
|
||||||
|
const filtered = all
|
||||||
|
.filter((l) => (state.onlyFav ? l.favorite : true))
|
||||||
|
.filter((l) => matches(l, q))
|
||||||
|
.sort(compare);
|
||||||
|
if (el.grid) el.grid.innerHTML = filtered.map(cardHtml).join("");
|
||||||
|
if (el.empty) el.empty.hidden = filtered.length !== 0;
|
||||||
|
if (el.meta) {
|
||||||
|
const favCount = all.filter((l) => l.favorite).length;
|
||||||
|
el.meta.textContent = `표시 ${filtered.length}개 · 전체 ${all.length}개 · 즐겨찾기 ${favCount}개`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(link) {
|
||||||
|
if (!el.modal) return;
|
||||||
|
el.modal.hidden = false;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
document.getElementById("modalTitle").textContent = link ? "링크 편집" : "링크 추가";
|
||||||
|
el.id.value = link ? link.id : "";
|
||||||
|
el.title.value = link ? link.title : "";
|
||||||
|
el.url.value = link ? link.url : "";
|
||||||
|
el.description.value = link ? link.description || "" : "";
|
||||||
|
el.tags.value = link ? (link.tags || []).join(", ") : "";
|
||||||
|
el.favorite.checked = link ? Boolean(link.favorite) : false;
|
||||||
|
setTimeout(() => el.title && el.title.focus(), 0);
|
||||||
|
}
|
||||||
|
function closeModal() {
|
||||||
|
if (!el.modal) return;
|
||||||
|
el.modal.hidden = true;
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
if (el.form) el.form.reset();
|
||||||
|
if (el.id) el.id.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBaseId(id) {
|
||||||
|
return baseOrder.has(id);
|
||||||
|
}
|
||||||
|
function getById(id) {
|
||||||
|
return getMergedLinks().find((l) => l.id === id) || null;
|
||||||
|
}
|
||||||
|
function setOverride(id, patch) {
|
||||||
|
state.store.overridesById[id] = Object.assign({}, state.store.overridesById[id] || {}, patch);
|
||||||
|
saveStore();
|
||||||
|
}
|
||||||
|
function upsertCustom(link) {
|
||||||
|
const n = normalizeLink(link);
|
||||||
|
const idx = (state.store.custom || []).findIndex((c) => c && c.id === n.id);
|
||||||
|
if (idx >= 0) state.store.custom[idx] = n;
|
||||||
|
else state.store.custom.push(n);
|
||||||
|
saveStore();
|
||||||
|
}
|
||||||
|
function delLink(id) {
|
||||||
|
if (isBaseId(id)) {
|
||||||
|
const s = new Set(state.store.tombstones || []);
|
||||||
|
s.add(id);
|
||||||
|
state.store.tombstones = Array.from(s);
|
||||||
|
saveStore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.store.custom = (state.store.custom || []).filter((c) => c && c.id !== id);
|
||||||
|
saveStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copy(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast("복사했습니다.");
|
||||||
|
} catch {
|
||||||
|
toast("복사에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportJson() {
|
||||||
|
const data = getMergedLinks();
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `links-export-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
a.remove();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importJsonText(text) {
|
||||||
|
const parsed = safeJsonParse(text, null);
|
||||||
|
const list = Array.isArray(parsed) ? parsed : parsed && Array.isArray(parsed.links) ? parsed.links : null;
|
||||||
|
if (!list) throw new Error("JSON 형식이 올바르지 않습니다.");
|
||||||
|
for (const item of list) {
|
||||||
|
if (!item) continue;
|
||||||
|
const n = normalizeLink(item);
|
||||||
|
// 충돌 방지
|
||||||
|
if (getById(n.id)) n.id = "import-" + Math.random().toString(16).slice(2);
|
||||||
|
state.store.custom.push(n);
|
||||||
|
}
|
||||||
|
saveStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const t = theme === "light" ? "light" : "dark";
|
||||||
|
document.documentElement.setAttribute("data-theme", t);
|
||||||
|
localStorage.setItem(THEME_KEY, t);
|
||||||
|
}
|
||||||
|
(function initTheme() {
|
||||||
|
const saved = localStorage.getItem(THEME_KEY);
|
||||||
|
if (saved === "light" || saved === "dark") return applyTheme(saved);
|
||||||
|
const prefersLight = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
|
applyTheme(prefersLight ? "light" : "dark");
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Wire events
|
||||||
|
if (el.q)
|
||||||
|
el.q.addEventListener("input", () => {
|
||||||
|
state.query = el.q.value || "";
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
if (el.sort)
|
||||||
|
el.sort.addEventListener("change", () => {
|
||||||
|
state.sortKey = el.sort.value || "json";
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
if (el.onlyFav)
|
||||||
|
el.onlyFav.addEventListener("change", () => {
|
||||||
|
state.onlyFav = Boolean(el.onlyFav.checked);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
if (el.btnAdd) el.btnAdd.addEventListener("click", () => openModal(null));
|
||||||
|
if (el.btnClose) el.btnClose.addEventListener("click", closeModal);
|
||||||
|
if (el.btnCancel) el.btnCancel.addEventListener("click", closeModal);
|
||||||
|
if (el.modal)
|
||||||
|
el.modal.addEventListener("click", (e) => {
|
||||||
|
const close = e.target && e.target.getAttribute && e.target.getAttribute("data-close");
|
||||||
|
if (close) closeModal();
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (el.form)
|
||||||
|
el.form.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = String(el.id.value || "").trim();
|
||||||
|
const title = String(el.title.value || "").trim();
|
||||||
|
const url = normalizeUrl(el.url.value);
|
||||||
|
if (!title) return toast("제목을 입력하세요.");
|
||||||
|
if (!url) return toast("URL을 입력하세요.");
|
||||||
|
const description = String(el.description.value || "").trim();
|
||||||
|
const tags = normalizeTags(el.tags.value);
|
||||||
|
const favorite = Boolean(el.favorite.checked);
|
||||||
|
const patch = { title, url, description, tags, favorite, updatedAt: nowIso() };
|
||||||
|
if (id) {
|
||||||
|
const cur = getById(id);
|
||||||
|
if (!cur) return toast("편집 대상이 없습니다.");
|
||||||
|
if (isBaseId(id)) setOverride(id, patch);
|
||||||
|
else upsertCustom(Object.assign({}, cur, patch));
|
||||||
|
toast("저장했습니다.");
|
||||||
|
} else {
|
||||||
|
upsertCustom({
|
||||||
|
id: "custom-" + Date.now() + "-" + Math.random().toString(16).slice(2),
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
favorite,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
});
|
||||||
|
toast("추가했습니다.");
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (el.grid)
|
||||||
|
el.grid.addEventListener("click", (e) => {
|
||||||
|
const btn = e.target.closest("[data-act]");
|
||||||
|
if (!btn) return;
|
||||||
|
const card = e.target.closest(".card");
|
||||||
|
if (!card) return;
|
||||||
|
const id = card.getAttribute("data-id");
|
||||||
|
const act = btn.getAttribute("data-act");
|
||||||
|
const link = id ? getById(id) : null;
|
||||||
|
if (!link) return;
|
||||||
|
if (act === "fav") {
|
||||||
|
const next = !link.favorite;
|
||||||
|
if (isBaseId(id)) setOverride(id, { favorite: next, updatedAt: nowIso() });
|
||||||
|
else upsertCustom(Object.assign({}, link, { favorite: next, updatedAt: nowIso() }));
|
||||||
|
render();
|
||||||
|
} else if (act === "copy") {
|
||||||
|
copy(link.url);
|
||||||
|
} else if (act === "edit") {
|
||||||
|
openModal(link);
|
||||||
|
} else if (act === "del") {
|
||||||
|
if (confirm(`삭제할까요?\n\n- ${link.title}`)) {
|
||||||
|
delLink(id);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (el.btnExport) el.btnExport.addEventListener("click", exportJson);
|
||||||
|
if (el.btnImport)
|
||||||
|
el.btnImport.addEventListener("click", () => {
|
||||||
|
if (el.file) el.file.click();
|
||||||
|
});
|
||||||
|
if (el.file)
|
||||||
|
el.file.addEventListener("change", async () => {
|
||||||
|
const f = el.file.files && el.file.files[0];
|
||||||
|
el.file.value = "";
|
||||||
|
if (!f) return;
|
||||||
|
try {
|
||||||
|
importJsonText(await f.text());
|
||||||
|
toast("가져오기 완료");
|
||||||
|
render();
|
||||||
|
} catch (err) {
|
||||||
|
toast(String(err && err.message ? err.message : "가져오기 실패"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (el.btnTheme)
|
||||||
|
el.btnTheme.addEventListener("click", () => {
|
||||||
|
const cur = document.documentElement.getAttribute("data-theme") || "dark";
|
||||||
|
applyTheme(cur === "dark" ? "light" : "dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
toast("스크립트 로딩 문제로 폴백 모드로 실행 중입니다.");
|
||||||
|
}, 200);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
(() => {
|
(() => {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// If index.html inline fallback already booted, do nothing.
|
||||||
|
if (globalThis.__LINKS_APP_BOOTED__) return;
|
||||||
// Mark boot so index.html fallback won't run
|
// Mark boot so index.html fallback won't run
|
||||||
globalThis.__LINKS_APP_BOOTED__ = true;
|
globalThis.__LINKS_APP_BOOTED__ = true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user