From 7322d42d9aa6cdc9c21489d5f946bfd0ef422808 Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sun, 16 Aug 2026 22:36:36 +0200 Subject: [PATCH] broken link fixer --- data/links.json | 49 +++++++++++++++++++++++++++ layouts/_default/baseof.html | 14 ++++++++ static/css/style.css | 59 ++++++++++++++++++++++++++++++++ static/js/link-status.js | 65 ++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 data/links.json create mode 100644 static/js/link-status.js diff --git a/data/links.json b/data/links.json new file mode 100644 index 0000000..808a301 --- /dev/null +++ b/data/links.json @@ -0,0 +1,49 @@ +{ + "broken": { + "http://ayende.com/Blog/Default.aspx": "https://web.archive.org/web/20110721180403/http://ayende.com/blog/default.aspx", + "http://barcampbodensee.mixxt.eu/": "https://web.archive.org/web/20101007123102/http://barcampbodensee.mixxt.eu/", + "http://barcampmunich.mixxt.de/": "https://web.archive.org/web/20100522000000/http://barcampmunich.mixxt.de/", + "http://blog.alexonasp.net/": "https://web.archive.org/web/20101206110511/http://blog.alexonasp.net/", + "http://blog.codemurai.de/": "https://web.archive.org/web/20120118132126/http://blog.codemurai.de/", + "http://blog.sacchet.de/": "https://web.archive.org/web/20110629153015/http://blog.sacchet.de/", + "http://blog.technewlogic.de/StaticContent/Pages/ObjectLounge_Main.aspx": "https://web.archive.org/web/20110725000000/http://blog.technewlogic.de/StaticContent/Pages/ObjectLounge_Main.aspx", + "http://code.google.com/p/webfinger/": "https://web.archive.org/web/20140720000000/http://code.google.com/p/webfinger/", + "http://codingdojo.org/cgi-bin/wiki.pl?KataPotter": "https://web.archive.org/web/20160907000000/http://codingdojo.org/cgi-bin/wiki.pl?KataPotter", + "http://der-albert.com/": "https://web.archive.org/web/20101129143710/http://der-albert.com/", + "http://jcselke.blogspot.com/": "https://web.archive.org/web/20110515000000/http://jcselke.blogspot.com/", + "http://netopenspace.de/2009": "https://web.archive.org/web/20091128173901/http://netopenspace.de/2009/", + "http://netopenspace.de/2009/": "https://web.archive.org/web/20091128173901/http://netopenspace.de/2009/", + "http://ogymes.com": "https://web.archive.org/web/20130822000000/http://ogymes.com/", + "http://ogymes.com/": "https://web.archive.org/web/20130822000000/http://ogymes.com/", + "http://ralfw.de/default.html": "https://web.archive.org/web/20110607000000/http://ralfw.de/default.html", + "http://techblog.tilllate.com/2008/08/14/webtuesday-lightning-talk-slides/": "https://web.archive.org/web/20120122000000/http://techblog.tilllate.com/2008/08/14/webtuesday-lightning-talk-slides/", + "http://topsy.com/tb/bit.ly/aYQGup": "https://web.archive.org/web/20131102000000/http://topsy.com/tb/bit.ly/aYQGup", + "http://topsy.com/tb/bit.ly/fA3cM": "https://web.archive.org/web/20131102000000/http://topsy.com/tb/bit.ly/fA3cM", + "http://ulm.netopenspace.de/": "https://web.archive.org/web/20100305000000/http://ulm.netopenspace.de/", + "http://ulm.netopenspace.de/2009/": "https://web.archive.org/web/20100305000000/http://ulm.netopenspace.de/2009/", + "http://weblog.drescher-hackel.de/": "https://web.archive.org/web/20110701000000/http://weblog.drescher-hackel.de/", + "http://weblogs.asp.net/ralfw/archive/tags/Software+Cells/default.aspx": "https://web.archive.org/web/20100805000000/http://weblogs.asp.net/ralfw/archive/tags/Software+Cells/default.aspx", + "http://wiki.movim.eu/index.php?title=Utilisateur:Otourly": "https://web.archive.org/web/20110427000000/http://wiki.movim.eu/index.php?title=Utilisateur:Otourly", + "http://www.altnetpodcast.com/episodes/18-talking-with-jeremy-miller-about-alt-net": "https://web.archive.org/web/20100927000000/http://www.altnetpodcast.com/episodes/18-talking-with-jeremy-miller-about-alt-net", + "http://www.artiso.com/problog/": "https://web.archive.org/web/20110812000000/http://www.artiso.com/problog/", + "http://www.aspnetzone.de/blogs/peterbucher/": "https://web.archive.org/web/20110809000000/http://www.aspnetzone.de/blogs/peterbucher/", + "http://www.bcnue2.de/": "https://web.archive.org/web/20121105000000/http://www.bcnue2.de/", + "http://www.be2.de/blog": "https://web.archive.org/web/20130207000000/http://www.be2.de/blog", + "http://www.be2.de/blog/": "https://web.archive.org/web/20130207000000/http://www.be2.de/blog/", + "http://www.be2.de/blog/barcamp/": "https://web.archive.org/web/20120214000000/http://www.be2.de/blog/barcamp/", + "http://www.der-softwareentwickler-blog.de/2010/03/26/kw12-giana-sisters-dumm-3-0-uml-visual-studio-2010-und-mehr/": "https://web.archive.org/web/20120418000000/http://www.der-softwareentwickler-blog.de/2010/03/26/kw12-giana-sisters-dumm-3-0-uml-visual-studio-2010-und-mehr/", + "http://www.dotnetpro.de/CurrentIssue.aspx": "https://web.archive.org/web/20160119224824/http://www.dotnetpro.de/CurrentIssue.aspx", + "http://www.in-your-face.org/": "https://web.archive.org/web/20110811000000/http://www.in-your-face.org/", + "http://www.lieser-online.de/blog/": "https://web.archive.org/web/20110815000000/http://www.lieser-online.de/blog/", + "http://www.ralfw.de/default.html": "https://web.archive.org/web/20110607000000/http://www.ralfw.de/default.html", + "http://www.webcamps.ms/#munich_panel": "https://web.archive.org/web/20100403000000/http://www.webcamps.ms/#munich_panel", + "http://www.webcamps.ms/Speaker/christianwenz": "https://web.archive.org/web/20100403000000/http://www.webcamps.ms/Speaker/christianwenz", + "http://www.webcamps.ms/Speaker/jongalloway": "https://web.archive.org/web/20100403000000/http://www.webcamps.ms/Speaker/jongalloway", + "https://loungerepo.codeplex.com/": "https://web.archive.org/web/20101221000000/https://loungerepo.codeplex.com/", + "https://www.computerworld.com/s/article/9135086/No_to_SQL_Anti_database_movement_gains_steam": "https://web.archive.org/web/20180612000000/https://www.computerworld.com/s/article/9135086/No_to_SQL_Anti_database_movement_gains_steam" + }, + "replacement": { + "http://www.codinghorror.com/blog/": "https://blog.codinghorror.com/", + "http://www.codinghorror.com/blog/2010/02/the-nonprogramming-programmer.html": "https://blog.codinghorror.com/the-nonprogramming-programmer/" + } +} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 506348b..3627363 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -95,5 +95,19 @@ + + + + + + + diff --git a/static/css/style.css b/static/css/style.css index 85ec045..c5a80ef 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -227,6 +227,65 @@ a:hover { text-decoration: underline; } .original-source { color: #999; margin-top: 30px; } +/* ==== Link-Status (broken) ==== */ +.post-content a.is-broken { + color: #a06020; + border-bottom: 1px dashed #c9853f; +} +.post-content a.is-broken:hover { + color: var(--accent); + text-decoration: none; +} + +/* ==== Broken-Link-Dialog ==== */ +.broken-link-dialog { + border: 0; + padding: 0; + border-radius: 8px; + box-shadow: 0 8px 30px rgba(0,0,0,.35); + max-width: 420px; + width: calc(100% - 40px); + color: #333; +} +.broken-link-dialog::backdrop { + background: rgba(0,0,0,.55); +} +.broken-link-dialog-inner { + padding: 24px; +} +.broken-link-title { + margin: 0 0 8px; + font-size: 1.05rem; + font-weight: bold; +} +.broken-link-url { + margin: 0 0 18px; + font-size: .85rem; + color: #777; + word-break: break-all; +} +.broken-link-actions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} +.broken-link-actions a { + color: var(--accent); + font-weight: 600; +} +.broken-link-actions a:hover { text-decoration: underline; } +.broken-link-actions button { + border: 1px solid #ccc; + background: #f5f5f5; + color: #333; + border-radius: 4px; + padding: 6px 14px; + cursor: pointer; + font-size: .9rem; +} +.broken-link-actions button:hover { background: #eaeaea; } + /* ==== Kommentare ==== */ .post-content h2:has(+ ol) { font-size: 1.5em; diff --git a/static/js/link-status.js b/static/js/link-status.js new file mode 100644 index 0000000..5897030 --- /dev/null +++ b/static/js/link-status.js @@ -0,0 +1,65 @@ +(function () { + var script = document.getElementById("link-status-data"); + if (!script) return; + var data; + try { data = JSON.parse(script.textContent); } catch (e) { return; } + + var broken = data.broken || {}; + var replacement = data.replacement || {}; + + var dialog = document.getElementById("broken-link-dialog"); + var urlEl = dialog ? document.getElementById("broken-link-url") : null; + var archiveEl = dialog ? document.getElementById("broken-link-archive") : null; + var closeBtn = dialog ? dialog.querySelector("[data-dialog-close]") : null; + + function variants(href) { + var list = [href]; + var www = href.replace(/^https?:\/\/www\./i, "https://"); + if (www !== href) list.push(www); + var noScheme = href.replace(/^https?:\/\//i, ""); + if (noScheme !== href) list.push(noScheme); + return list; + } + + function lookup(href) { + var vs = variants(href); + for (var i = 0; i < vs.length; i++) { + if (Object.prototype.hasOwnProperty.call(broken, vs[i])) return { status: "broken", key: vs[i] }; + if (Object.prototype.hasOwnProperty.call(replacement, vs[i])) return { status: "replacement", key: vs[i] }; + } + return null; + } + + var content = document.querySelector(".post-content"); + if (!content) return; + + var links = content.querySelectorAll("a[href^='http']"); + Array.prototype.forEach.call(links, function (a) { + var href = a.getAttribute("href"); + var hit = lookup(href); + if (!hit) return; + + if (hit.status === "broken") { + a.classList.add("is-broken"); + a.setAttribute("aria-label", (a.textContent || "Link") + " (die URL scheint nicht mehr verfügbar zu sein)"); + a.addEventListener("click", function (ev) { + ev.preventDefault(); + if (!dialog) return; + var target = a.getAttribute("href"); + urlEl.textContent = target; + archiveEl.href = "https://web.archive.org/web/*/" + target; + dialog.showModal(); + }); + } else if (hit.status === "replacement") { + var rep = replacement[hit.key]; + if (rep) a.setAttribute("href", rep); + } + }); + + if (dialog) { + dialog.addEventListener("click", function (ev) { + if (ev.target === dialog) dialog.close(); + }); + if (closeBtn) closeBtn.addEventListener("click", function () { dialog.close(); }); + } +})();