PageSourceSearch

https://frenkd.github.io/assets/js/substack-feed.js

js frenkd.github.io collected 2026-10-03 09:12:56 UTC 5,606 bytes, 141 lines download raw bytes

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.