diff --git a/backend/go.mod b/backend/go.mod index 1288888..1cb3fe8 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -34,8 +34,10 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect lukechampine.com/blake3 v1.1.6 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 72382c0..e8c7bef 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -69,12 +69,22 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go index 5bebd64..617df6a 100644 --- a/backend/internal/api/handler.go +++ b/backend/internal/api/handler.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "html" "io" "net/http" "net/url" @@ -13,6 +14,7 @@ import ( "time" "github.com/go-chi/chi/v5" + xcharset "golang.org/x/net/html/charset" "margin.at/internal/analytics" "margin.at/internal/config" @@ -1069,27 +1071,46 @@ func (h *Handler) fetchURLMetadata(ctx context.Context, targetURL string) map[st return map[string]string{"title": ""} } - content := string(body) + enc, _, _ := xcharset.DetermineEncoding(body, resp.Header.Get("Content-Type")) + decoded, err := enc.NewDecoder().Bytes(body) + if err != nil { + decoded = body + } + + content := string(decoded) + + extractContent := func(rest string) string { + for _, prefix := range []string{"content=\"", "content='"} { + if contentIdx := strings.Index(rest, prefix); contentIdx != -1 { + quote := prefix[len(prefix)-1] + start := contentIdx + len(prefix) + if end := strings.IndexByte(rest[start:], quote); end != -1 { + return html.UnescapeString(rest[start : start+end]) + } + } + } + return "" + } extract := func(key string) string { - attr := fmt.Sprintf("property=\"og:%s\"", key) - if idx := strings.Index(content, attr); idx != -1 { - rest := content[idx:] - if contentIdx := strings.Index(rest, "content=\""); contentIdx != -1 { - start := contentIdx + 9 - if end := strings.Index(rest[start:], "\""); end != -1 { - return rest[start : start+end] + for _, attr := range []string{ + fmt.Sprintf("property=\"og:%s\"", key), + fmt.Sprintf("property='og:%s'", key), + } { + if idx := strings.Index(content, attr); idx != -1 { + if v := extractContent(content[idx:]); v != "" { + return v } } } - attr = fmt.Sprintf("name=\"%s\"", key) - if idx := strings.Index(content, attr); idx != -1 { - rest := content[idx:] - if contentIdx := strings.Index(rest, "content=\""); contentIdx != -1 { - start := contentIdx + 9 - if end := strings.Index(rest[start:], "\""); end != -1 { - return rest[start : start+end] + for _, attr := range []string{ + fmt.Sprintf("name=\"%s\"", key), + fmt.Sprintf("name='%s'", key), + } { + if idx := strings.Index(content, attr); idx != -1 { + if v := extractContent(content[idx:]); v != "" { + return v } } } @@ -1101,7 +1122,7 @@ func (h *Handler) fetchURLMetadata(ctx context.Context, targetURL string) map[st if idx := strings.Index(content, ""); idx != -1 { start := idx + 7 if end := strings.Index(content[start:], ""); end != -1 { - title = content[start : start+end] + title = html.UnescapeString(strings.TrimSpace(content[start : start+end])) } } } diff --git a/web/src/components/common/Card.tsx b/web/src/components/common/Card.tsx index 7202e2f..0c5a48c 100644 --- a/web/src/components/common/Card.tsx +++ b/web/src/components/common/Card.tsx @@ -332,20 +332,16 @@ export default function Card({ : null; const decodeHTMLEntities = (text: string) => { - const entities: Record = { - "&": "&", - "<": "<", - ">": ">", - """: '"', - "'": "'", - "'": "'", - "/": "/", - " ": " ", - }; - return text.replace( - /&(?:amp|lt|gt|quot|nbsp|#39|#x27|#x2F);/g, - (match) => entities[match] || match, - ); + if (!text.includes("&")) return text; + try { + const doc = new DOMParser().parseFromString( + `${text}`, + "text/html", + ); + return doc.body.textContent ?? text; + } catch { + return text; + } }; const displayTitle = decodeHTMLEntities(