this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Handle youtube links and 404s

+135 -51
+32
internal/assets/css/screen.css
··· 314 314 display: inline-block; 315 315 } 316 316 317 + .og-image.is-video { 318 + cursor: pointer; 319 + } 320 + 317 321 .og-image img { 318 322 max-width: 100%; 319 323 border-radius: 8px; /* Slack likes rounder corners on media */ ··· 349 353 border-bottom: 9px solid transparent; 350 354 margin-left: 4px; /* Optical centering */ 351 355 } 356 + 357 + /* HTTP Error Styles */ 358 + .og-error-compact { 359 + margin-top: 4px; 360 + margin-bottom: 4px; 361 + font-size: 13px; 362 + font-family: monospace; 363 + } 364 + 365 + .http-error-badge { 366 + display: inline-block; 367 + padding: 2px 6px; 368 + border-radius: 4px; 369 + font-weight: bold; 370 + color: white; 371 + background-color: #d72b3f; /* Red for errors */ 372 + } 373 + 374 + .broken-link { 375 + color: #868686; 376 + text-decoration: none; 377 + margin-left: 5px; 378 + } 379 + 380 + .broken-link:hover { 381 + text-decoration: underline; 382 + color: #666; 383 + }
+25
internal/handler/preview.go
··· 45 45 } 46 46 defer resp.Body.Close() 47 47 48 + if resp.StatusCode >= 400 { 49 + w.Header().Set("Content-Type", "application/json") 50 + json.NewEncoder(w).Encode(map[string]interface{}{ 51 + "error": "HTTP Error", 52 + "status": resp.StatusCode, 53 + }) 54 + return 55 + } 56 + 48 57 // Parse HTML 49 58 doc, err := html.Parse(resp.Body) 50 59 if err != nil { ··· 130 139 } 131 140 } 132 141 f(doc) 142 + 143 + // Check for YouTube "soft 404" (Video unavailable) 144 + // YouTube returns 200 but minimal metadata for unavailable videos. 145 + if strings.Contains(urlParam, "youtube.com") || strings.Contains(urlParam, "youtu.be") { 146 + title, hasTitle := metadata["title"] 147 + // Valid videos usually have a specific title in og:title or title tag. 148 + // Unavailable videos often have just "- YouTube" or no og:title. 149 + if !hasTitle || title == " - YouTube" || title == "YouTube" { 150 + w.Header().Set("Content-Type", "application/json") 151 + json.NewEncoder(w).Encode(map[string]interface{}{ 152 + "error": "Video Unavailable", 153 + "status": 404, 154 + }) 155 + return 156 + } 157 + } 133 158 134 159 json.NewEncoder(w).Encode(metadata) 135 160 }
+2 -23
internal/service/content.go
··· 111 111 } 112 112 } 113 113 114 - // YouTube 115 - // Supports: youtube.com/watch?v=, embed/, youtu.be/ 116 - videoID := "" 117 - if strings.Contains(strings.ToLower(item.URL), "youtube.com") || strings.Contains(strings.ToLower(item.URL), "youtu.be") { 118 - re := regexp.MustCompile(`(?:youtube\.com\/watch\?v=|youtube\.com\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})`) 119 - matches := re.FindStringSubmatch(item.URL) 120 - if len(matches) > 1 { 121 - videoID = matches[1] 122 - } else { 123 - // Try query param 124 - re2 := regexp.MustCompile(`youtube\.com\/watch\?.*[&?]v=([a-zA-Z0-9_-]{11})`) 125 - matches2 := re2.FindStringSubmatch(item.URL) 126 - if len(matches2) > 1 { 127 - videoID = matches2[1] 128 - } 129 - } 130 - } 131 - 132 - if videoID != "" { 133 - embed := fmt.Sprintf(`<div class="youtube-embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/%s?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen"></iframe></div>`, videoID) 134 - d.Content = template.HTML(embed) 135 - isYoutube = true 136 - } 114 + // YouTube logic removed: Handled client-side by OGPreview for "click to play" behavior 115 + // and to correctly handle unavailable videos (404s). 137 116 138 117 if !isYoutube && !isTwitter && !isImgur { 139 118 baseURL := s.Config.BaseURL
+76 -28
internal/templates/views/index.html
··· 9 9 var _gaq = _gaq || []; 10 10 _gaq.push(['_setAccount', 'UA-24593498-1']); 11 11 _gaq.push(['_trackPageview']); 12 - 12 + 13 13 (function() { 14 14 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 15 15 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; ··· 18 18 </script> 19 19 <!-- Twitter Cards --> 20 20 <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script> 21 - 21 + 22 22 <!-- Open Graph Preview Loader --> 23 23 <script type="text/javascript"> 24 24 (function() { ··· 26 26 var url = item.getAttribute('data-url'); 27 27 var contentType = item.getAttribute('data-content-type'); 28 28 var previewDiv = item.querySelector('.og-preview'); 29 - 29 + 30 30 // Only load preview for non-image content (text/html) 31 31 if (!url || (contentType && contentType.match(/image/))) { 32 32 return; 33 33 } 34 - 35 - // Skip OG preview for YouTube links (they're embedded directly) 36 - if (url.match(/youtube\.com|youtu\.be/i)) { 37 - return; 38 - } 34 + 35 + // Skip OG preview for YouTube links check removed to allow client-side handling 39 36 40 37 // Ensure URL has protocol 41 38 if (!url.match(/^https?:\/\//)) { 42 39 url = 'http://' + url; 43 40 } 44 - 45 - // Create preview endpoint URL 46 - var previewUrl = '/ogpreview.cgi?url=' + encodeURIComponent(url); 47 - 48 - // Load preview asynchronously 41 + 49 42 var xhr = new XMLHttpRequest(); 43 + // Add timestamp to prevent caching of previous 200 OK responses 44 + var previewUrl = '/ogpreview.cgi?url=' + encodeURIComponent(url) + '&_t=' + new Date().getTime(); 50 45 xhr.open('GET', previewUrl, true); 51 46 xhr.onreadystatechange = function() { 52 47 if (xhr.readyState === 4 && xhr.status === 200) { 53 48 try { 54 49 var data = JSON.parse(xhr.responseText); 50 + 51 + // Handle HTTP Errors (compact display) 52 + if (data.status && data.status >= 400) { 53 + var errorHtml = '<div class="og-error-compact">'; 54 + errorHtml += '<span class="http-error-badge">' + data.status + '</span> '; 55 + errorHtml += '<a href="' + escapeHtml(url) + '" class="broken-link" target="_blank">' + escapeHtml(url) + '</a>'; 56 + errorHtml += '</div>'; 57 + previewDiv.innerHTML = errorHtml; 58 + 59 + // Hide the original link text to reduce clutter 60 + var originalLink = element.querySelector('.link'); 61 + if (originalLink) { 62 + originalLink.style.display = 'none'; 63 + } 64 + return; 65 + } 66 + 55 67 if (data.error) { 56 68 return; 57 69 } 58 - 70 + 59 71 // Use Open Graph image, Twitter image, or nothing 60 72 var imageUrl = data.image || data.twitter_image || null; 61 73 // Use Open Graph title, Twitter title, or fallback title 62 74 var title = data.title || data.twitter_title || null; 63 75 // Use Open Graph description, Twitter description, or fallback description 64 76 var description = data.description || data.twitter_description || null; 65 - 77 + 66 78 // Only show preview if we have at least an image, title, or description 67 79 if (!imageUrl && !title && !description) { 68 80 return; 69 81 } 70 - 71 - // Build preview HTML 82 + 72 83 // Build preview HTML 73 84 var previewHTML = '<div class="og-card">'; 74 - 85 + 75 86 // Site Info (Icon + Name) 76 87 var provider = data.provider_name || 'Link'; 77 - var iconUrl = '/favicon.ico'; 88 + var iconUrl = '/favicon.ico'; 78 89 if (provider === 'Reddit') iconUrl = 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png'; 79 - 90 + 80 91 previewHTML += '<div class="og-site-info"><img src="' + iconUrl + '" class="og-site-icon" /> ' + escapeHtml(provider) + '</div>'; 81 92 82 93 // Title ··· 95 106 // Image 96 107 if (imageUrl) { 97 108 var imageContent = '<img src="' + escapeHtml(imageUrl) + '" alt="" />'; 98 - // If it's a video, add the play button overlay 99 - if (data.type === 'video') { 109 + var extraClass = ''; 110 + var onClickAttr = ''; 111 + 112 + // Video / YouTube handling 113 + if (data.type === 'video' || url.match(/(youtube\.com|youtu\.be)/)) { 114 + extraClass = 'is-video'; 100 115 imageContent += '<div class="og-play-button"></div>'; 116 + 117 + // Click to play logic - safe DOM lookup 118 + onClickAttr = 'onclick="replaceWithVideo(this)"'; 101 119 } 102 - previewHTML += '<div class="og-image ' + (data.type === 'video' ? 'is-video' : '') + '">' + imageContent + '</div>'; 120 + 121 + previewHTML += '<div class="og-image ' + extraClass + '" ' + onClickAttr + '>' + imageContent + '</div>'; 103 122 } 104 - 123 + 105 124 previewHTML += '</div>'; 106 - 125 + 107 126 previewDiv.innerHTML = previewHTML; 108 127 } catch (e) { 109 128 // Silently fail if JSON parsing fails 129 + console.error(e); 110 130 } 111 131 } 112 132 }; 113 133 xhr.send(); 114 134 } 115 - 135 + 116 136 function escapeHtml(text) { 117 137 var div = document.createElement('div'); 118 138 div.textContent = text; 119 139 return div.innerHTML; 120 140 } 121 - 141 + 142 + // Global handler for video replacemen 143 + window.replaceWithVideo = function(container) { 144 + // Find URL from parent item 145 + var item = container.closest('.item'); 146 + if (!item) return; 147 + var url = item.getAttribute('data-url'); 148 + if (!url) return; 149 + 150 + var videoID = ''; 151 + var match = url.match(/(?:youtube\.com\/watch\?v=|youtube\.com\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/); 152 + if (match) { 153 + videoID = match[1]; 154 + } else { 155 + // Try query param scan too 156 + var match2 = url.match(/v=([a-zA-Z0-9_-]{11})/); 157 + if (match2) videoID = match2[1]; 158 + } 159 + 160 + if (videoID) { 161 + var iframe = '<div class="youtube-embed-wrapper"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/' + videoID + '?autoplay=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>'; 162 + container.innerHTML = iframe; 163 + container.onclick = null; // Remove handler 164 + container.classList.remove('is-video'); 165 + container.style.cursor = 'default'; 166 + } 167 + }; 168 + 169 + 122 170 // Load all previews when DOM is ready 123 171 function initOGPreviews() { 124 172 var items = document.querySelectorAll('.item[data-url]'); ··· 126 174 loadOGPreview(items[i]); 127 175 } 128 176 } 129 - 177 + 130 178 // Run when DOM is ready 131 179 if (document.readyState === 'loading') { 132 180 document.addEventListener('DOMContentLoaded', initOGPreviews);