feat(cards): X/Twitter oEmbed support and image URL card detection; render oEmbed HTML in UI

This commit is contained in:
Thomas Cravey 2025-08-17 16:03:22 -05:00
parent 9e95ccdca4
commit 15f7f3ac96
2 changed files with 37 additions and 1 deletions

View file

@ -215,6 +215,7 @@ type linkCard struct {
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
HTML string `json:"html"`
}
// handleLinkCard fetches basic OpenGraph/Twitter card metadata (best-effort) and returns a small card.
@ -246,9 +247,33 @@ func (s *Server) handleLinkCard(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(s.cardCache[raw])
return
}
// Special handling for X/Twitter posts via oEmbed
host := strings.ToLower(u.Host)
if (host == "x.com" || host == "twitter.com" || strings.HasSuffix(host, ".twitter.com")) && strings.Contains(strings.ToLower(u.Path), "/status/") {
oembed := "https://publish.twitter.com/oembed?omit_script=1&hide_thread=1&dnt=1&align=center&url=" + url.QueryEscape(raw)
client := &http.Client{Timeout: 8 * time.Second}
reqTw, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, oembed, nil)
respTw, errTw := client.Do(reqTw)
if errTw == nil && respTw.StatusCode >= 200 && respTw.StatusCode < 300 {
defer respTw.Body.Close()
var o struct{ HTML string `json:"html"` }
if err := json.NewDecoder(respTw.Body).Decode(&o); err == nil && o.HTML != "" {
card := linkCard{URL: raw, HTML: o.HTML}
s.cardCache[raw] = card
s.cardCacheExp[raw] = time.Now().Add(24 * time.Hour)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(card)
return
}
}
// fallthrough to generic fetch if oEmbed fails
}
// fetch minimal HTML and extract tags using a tolerant HTML parser
client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; sojuboy/1.0)")
resp, err := client.Do(req)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
@ -261,6 +286,16 @@ func (s *Server) handleLinkCard(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("bad status"))
return
}
// If image content-type, return as image card
if ct := strings.ToLower(resp.Header.Get("Content-Type")); strings.HasPrefix(ct, "image/") {
card := linkCard{URL: raw, Image: raw}
s.cardCache[raw] = card
s.cardCacheExp[raw] = time.Now().Add(24 * time.Hour)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(card)
return
}
// limit to 256KB and parse tokens
limited := http.MaxBytesReader(w, resp.Body, 262144)
doc, err := xhtml.Parse(limited)