1document.addEventListener('DOMContentLoaded', function () { 2 var feedEl = document.getElementById('substack-feed'); 3 if (!feedEl) return; 4 5 var substackUrl = feedEl.getAttribute('data-substack-url'); 6 if (!substackUrl) return; 7 8 var feedUrl = substackUrl.replace(/\/$/, '') + '/feed'; 9 var proxyUrl = 'https://api.allorigins.win/get?url=' + encodeURIComponent(feedUrl); 10 11 var settled = false; 12 var fallbackTimer = setTimeout(function () { 13 if (!settled) { settled = true; displayError(); } 14 }, 7000); 15 16 fetch(proxyUrl) 17 .then(function (r) { 18 if (!r.ok) throw new Error('proxy ' + r.status); 19 return r.json(); 20 }) 21 .then(function (data) { 22 if (settled) return; // timeout already showed fallback 23 settled = true; 24 clearTimeout(fallbackTimer); 25 var xml = data.contents || ''; 26 if (!xml) throw new Error('empty feed'); 27 var doc = new DOMParser().parseFromString(xml, 'text/xml'); 28 var items = Array.from(doc.querySelectorAll('item')); 29 if (!items.length) throw new Error('no items'); 30 renderPosts(items.slice(0, 5)); 31 }) 32 .catch(function (err) { 33 if (settled) return; 34 settled = true; 35 clearTimeout(fallbackTimer); 36 console.error('Substack feed error:', err); 37 displayError(); 38 }); 39 40 function getText(el, tag) { 41 var node = el.querySelector(tag); 42 return node ? (node.textContent || '').trim() : ''; 43 } 44 45 function parseItem(item) { 46 var title = getText(item, 'title'); 47 var link = getText(item, 'link') || getText(item, 'guid'); 48 var pubDate = getText(item, 'pubDate'); 49 var desc = getText(item, 'description'); 50 51 // Enclosure image (direct S3 URL â no proxy needed) 52 var thumbUrl = ''; 53 var enclosure = item.querySelector('enclosure'); 54 if (enclosure) { 55 var u = enclosure.getAttribute('url') || ''; 56 if (u && !u.includes('youtube.com')) thumbUrl = u; 57 } 58 // Fallback: first img src from content:encoded 59 if (!thumbUrl) { 60 var encoded = getText(item, 'encoded'); 61 var m = encoded.match(/src="(https:\/\/substack-post-media\.s3\.amazonaws\.com\/[^"]+)"/); 62 if (m) thumbUrl = m[1]; 63 } 64 65 var date = pubDate ? new Date(pubDate) : null; 66 var shortDate = date 67 ? date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) 68 : ''; 69 70 var tmp = document.createElement('div'); 71 tmp.innerHTML = desc; 72 var excerpt = (tmp.textContent || '').trim().replace(/\s+/g, ' '); 73 if (excerpt.length > 150) excerpt = excerpt.slice(0, 150) + 'â¦'; 74 75 return { title: title, link: link, shortDate: shortDate, excerpt: excerpt, thumbUrl: thumbUrl }; 76 } 77 78 function renderPosts(items) { 79 var container = document.getElementById('substack-posts'); 80 if (!container) return; 81 container.innerHTML = ''; 82 83 var parsed = items.map(parseItem); 84 85 // ââ Phase 1: render text immediately, images start empty (shimmer) ââ 86 parsed.forEach(function (post) { 87 var row = document.createElement('div'); 88 row.className = 'item-row item-row-media'; 89 // Only use data-fetch-thumb proxy if there is no direct enclosure URL 90 if (!post.thumbUrl) row.setAttribute('data-fetch-thumb', post.link); 91 92 row.innerHTML = 93 '<a href="' + post.link + '" target="_blank" rel="noopener"' + 94 ' class="item-thumb-wrap"' + 95 ' data-fallback="/assets/images/fallbacks/substack.png"' + 96 ' tabindex="-1" aria-hidden="true">' + 97 '<img class="item-thumb" src="" alt=""' + 98 ' width="96" height="54" decoding="async"' + 99 ' onload="this.setAttribute(\'data-loaded\',\'1\')"' + 100 ' onerror="this.onerror=null;this.src=\'/assets/images/fallbacks/substack.png\';this.setAttribute(\'data-loaded\',\'1\')">' + 101 '</a>' + 102 '<div class="item-thumb-body">' + 103 '<div class="item-header">' + 104 '<span class="item-title">' + post.title + '</span>' + 105 '<span class="item-meta">' + post.shortDate + '</span>' + 106 '</div>' + 107 '<p class="item-desc">' + post.excerpt + '</p>' + 108 '<a href="' + post.link + '" class="item-link" style="font-size:.8125rem" target="_blank">' + 109 'Read post' + 110 '<svg style="display:inline;vertical-align:middle;opacity:.6;margin-left:3px" width="10" height="10" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
110' + 111 '</a>' + 112 '</div>'; 113 114 container.appendChild(row); 115 }); 116 117 // ââ Phase 2: load images after text is painted ââ 118 requestAnimationFrame(function () { 119 var rows = container.querySelectorAll('.item-row'); 120 parsed.forEach(function (post, i) { 121 var img = rows[i] && rows[i].querySelector('.item-thumb'); 122 if (!img) return; 123 if (post.thumbUrl) { 124 // Direct S3 URL â browser loads it natively, no proxy needed 125 img.src = post.thumbUrl; 126 } 127 // Items without a thumbUrl already have data-fetch-thumb set; 128 // fetchThumnails (called below) will proxy-fetch them. 129 }); 130 131 if (window.fetchThumbnails) window.fetchThumbnails(container); 132 }); 133 } 134 135 function displayError() { 136 var container = document.getElementById('substack-posts'); 137 if (!container) return; 138 container.innerHTML = 139 '<a href="' + substackUrl + '/archive" target="_blank" rel="noopener" style="color:var(--text-muted);text-decoration:underline;text-underline-offset:2px">View on Substack â</a>'; 140 } 141});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.