···3939 DateMonth string `json:"date_month"` // e.g. "Jan"
4040 DateRawDay string `json:"date_raw_day"` // e.g. "01"
4141 DateYear string `json:"date_year"` // e.g. "2026"
4242+ SuppressOG bool `json:"suppress_og"`
4243}
43444445func NewContentService(cfg *config.Config) *ContentService {
···8788 embed := fmt.Sprintf(`<blockquote class="twitter-tweet"><a href="%s" target="_blank">%s</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, embedURL, item.Title)
8889 d.Content = template.HTML(embed)
8990 isTwitter = true
9191+ d.SuppressOG = true
9092 }
9193 }
92949395 // Imgur
9496 isImgur := false
9597 if strings.Contains(item.URL, "imgur.com") {
9696- // Regex for ID extraction
9797- re := regexp.MustCompile(`imgur\.com\/(?:.*[\/-])?([a-zA-Z0-9]{5,})(?:\..*)?$`)
9898- matches := re.FindStringSubmatch(item.URL)
9999- if len(matches) > 1 {
100100- id := matches[1]
101101- videoURL := fmt.Sprintf("https://i.imgur.com/%s.mp4", id)
102102- imgURL := fmt.Sprintf("https://i.imgur.com/%s.jpg", id)
103103-104104- // We render a video tag by default. If it fails to load (404 for static images, or other errors),
105105- // the onerror handler swaps it for a standard image tag.
106106- // This avoids server-side rate limits (HTTP 429) and speeds up response time.
107107- // Note: We wrap it in the anchor tag in the Go code, but the onerror replaces the VIDEO tag specifically.
9898+ // Gallery Check
9999+ if strings.Contains(item.URL, "/gallery/") || strings.Contains(item.URL, "/a/") {
100100+ // It is a gallery
101101+ // Create a nice looking card for the gallery
102102+ // <i class="fa-regular fa-images"></i> is a font-awesome icon if available, but let's stick to text/SVG or existing styles.
103103+ // converting to simple link with a visual cue
108104 embed := fmt.Sprintf(
109109- `<a href="%s" target="_blank"><video autoplay loop muted playsinline style="max-width: 500px;" src="%s" onerror="this.onerror=null;this.outerHTML='<img src=\'%s\' style=\'max-width: 500px;\' />'"></video></a>`,
110110- item.URL, videoURL, imgURL)
105105+ `<span class="imgur-gallery-card" style="display: inline-block; overflow: hidden; border: 1px solid #444; border-radius: 5px; background-color: #222; max-width: 400px; width: 100%%; vertical-align: top;">
106106+ <a href="%s" target="_blank" style="color: #fff; text-decoration: none; display: block;">
107107+ <span class="gallery-image-container" style="display: block; background-color: #000; text-align: center; min-height: 200px; line-height: 200px;">
108108+ <span style="font-size: 48px;">📸</span>
109109+ </span>
110110+ <span style="display: block; padding: 10px;">
111111+ <span style="font-weight: bold; display: block;">Imgur Gallery</span>
112112+ <span style="font-size: 0.9em; opacity: 0.8; display: block;">%s</span>
113113+ </span>
114114+ </a>
115115+ </span>`, item.URL, item.Title)
111116112117 d.Content = template.HTML(embed)
113118 isImgur = true
119119+ d.SuppressOG = true
120120+121121+ } else {
122122+ // Single Image / Video Detection
123123+ re := regexp.MustCompile(`imgur\.com\/(?:.*[\/-])?([a-zA-Z0-9]{5,})(?:\..*)?$`)
124124+ matches := re.FindStringSubmatch(item.URL)
125125+ if len(matches) > 1 {
126126+ id := matches[1]
127127+ videoURL := fmt.Sprintf("https://i.imgur.com/%s.mp4", id)
128128+ imgURL := fmt.Sprintf("https://i.imgur.com/%s.jpg", id)
129129+130130+ // We render a video tag by default. If it fails to load (404 for static images, or other errors),
131131+ // the onerror handler swaps it for a standard image tag.
132132+ // This avoids server-side rate limits (HTTP 429) and speeds up response time.
133133+ // Note: We wrap it in the anchor tag in the Go code, but the onerror replaces the VIDEO tag specifically.
134134+ embed := fmt.Sprintf(
135135+ `<a href="%s" target="_blank"><video autoplay loop muted playsinline style="max-width: 500px;" src="%s" onerror="this.onerror=null;this.outerHTML='<img src=\'%s\' style=\'max-width: 500px;\' />'"></video></a>`,
136136+ item.URL, videoURL, imgURL)
137137+138138+ d.Content = template.HTML(embed)
139139+ isImgur = true
140140+ d.SuppressOG = true
141141+ }
114142 }
115143 }
116144···129157 embed := fmt.Sprintf(`<a href="%s" target="_blank"><img src="%s" alt="%s" /></a>`, photoPage, item.URL, item.Title)
130158 d.Content = template.HTML(embed)
131159 isFlickr = true
160160+ d.SuppressOG = true
132161 }
133162 }
134163
+33-3
internal/templates/views/index.html
···4545 var contentType = item.getAttribute("data-content-type");
4646 var ircId = item.getAttribute("data-irc-link-id");
4747 var previewDiv = item.querySelector(".og-preview");
4848+ var imgurCard = item.querySelector(".imgur-gallery-card");
48494950 // Only load preview for non-image content (text/html)
5050- if (!url || (contentType && contentType.match(/image/))) {
5151+ // Exception: Imgur Gallery Cards need to fetch the cover image even if "image" is in content type
5252+ if (!url || (contentType && contentType.match(/image/) && !imgurCard)) {
5353+ return;
5454+ }
5555+5656+ // If no preview div AND no imgur card, nothing to do
5757+ if (!previewDiv && !imgurCard) {
5158 return;
5259 }
5360···8491 if (data.error || (data.status && data.status >= 400)) {
8592 var status = data.status || 404;
8693 var linkSpan = item.querySelector(".link");
8787- if (linkSpan) {
9494+ if (linkSpan && !imgurCard) {
8895 var statusPrefix = '<span class="http-error-badge">' + status + '</span> ';
8996 linkSpan.innerHTML = '<a href="' + escapeHtml(url) + '" class="missing-link" target="_blank">' + statusPrefix + escapeHtml(url) + '</a>';
9097 }
9191- previewDiv.remove();
9898+ if (previewDiv) previewDiv.remove();
9299 return;
93100 }
94101···124131 var imageUrl = data.image || data.twitter_image || null;
125132 var title = data.title || data.twitter_title || null;
126133 var description = data.description || data.twitter_description || null;
134134+135135+ // SPECIAL HANDLING: Imgur Gallery Card
136136+ if (imgurCard) {
137137+ if (imageUrl) {
138138+ // Find the placeholder container
139139+ // .gallery-image-container
140140+ var imgContainer = imgurCard.querySelector(".gallery-image-container");
141141+ if (imgContainer) {
142142+ // Replace specific placeholder or just set innerHTML
143143+ // We want to replace the whole container contents (camera icon) with the image
144144+ // and ensure styles are good.
145145+ var imgHtml = '<img src="' + escapeHtml(imageUrl) + '" style="width: 100%; height: auto; display: block; object-fit: cover;" />';
146146+ imgContainer.innerHTML = imgHtml;
147147+ // Remove min-height/centering styles from container if needed, or keeping them is fine if image covers it.
148148+ imgContainer.style.minHeight = "auto";
149149+ imgContainer.style.lineHeight = "normal";
150150+ imgContainer.style.backgroundColor = "transparent";
151151+ }
152152+ }
153153+ return;
154154+ }
155155+156156+ if (!previewDiv) return;
127157128158 // Only show preview if we have at least an image, title, or description
129159 if (!imageUrl && !title && !description && !data.embed_html) {
+1-1
internal/templates/views/tumble_item_ircLink.html
···11 <div class="item" data-url="{{.URL}}" data-content-type="{{.ContentType}}" data-irc-link-id="{{.ID}}">
22 <span class="link">{{.Content}}</span>
33 <span class="author"><a href="/?poster={{.User}}" data-tooltip="{{.FormattedDate}}">{{.User}}</a></span>
44- <div class="og-preview" id="og-preview-{{.ID}}"></div>
44+ {{ if not .SuppressOG }}<div class="og-preview" id="og-preview-{{.ID}}"></div>{{ end }}
55 </div>