(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

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

better og image for collections and highlights and some minor order swaps done for annotations/bookmarks

scanash00 0254d6aa bf028273

+207 -16
+207 -16
backend/internal/api/og.go
··· 719 719 } 720 720 } 721 721 722 - text = "Highlight" 722 + targetTitle := "" 723 + if highlight.TargetTitle != nil { 724 + targetTitle = *highlight.TargetTitle 725 + } 726 + 723 727 if highlight.SelectorJSON != nil && *highlight.SelectorJSON != "" { 724 728 var selector struct { 725 729 Exact string `json:"exact"` ··· 734 738 sourceDomain = parsed.Host 735 739 } 736 740 } 741 + 742 + img := generateHighlightOGImagePNG(authorHandle, targetTitle, quote, sourceDomain, avatarURL) 743 + 744 + w.Header().Set("Content-Type", "image/png") 745 + w.Header().Set("Cache-Control", "public, max-age=86400") 746 + png.Encode(w, img) 747 + return 737 748 } else { 738 749 collection, err := h.db.GetCollectionByURI(uri) 739 750 if err == nil && collection != nil { ··· 752 763 if collection.Icon != nil && *collection.Icon != "" { 753 764 icon = iconToEmoji(*collection.Icon) 754 765 } 755 - text = fmt.Sprintf("%s %s", icon, collection.Name) 766 + 767 + description := "" 756 768 if collection.Description != nil && *collection.Description != "" { 757 - quote = *collection.Description 769 + description = *collection.Description 758 770 } 771 + 772 + img := generateCollectionOGImagePNG(authorHandle, collection.Name, description, icon, avatarURL) 773 + 774 + w.Header().Set("Content-Type", "image/png") 775 + w.Header().Set("Cache-Control", "public, max-age=86400") 776 + png.Encode(w, img) 777 + return 759 778 } else { 760 779 http.Error(w, "Record not found", http.StatusNotFound) 761 780 return ··· 815 834 816 835 contentWidth := width - (padding * 2) 817 836 837 + if text != "" { 838 + if len(text) > 300 { 839 + text = text[:297] + "..." 840 + } 841 + lines := wrapTextToWidth(text, contentWidth, 32) 842 + for i, line := range lines { 843 + if i >= 6 { 844 + break 845 + } 846 + drawText(img, line, padding, yPos+(i*42), textPrimary, 32, false) 847 + } 848 + yPos += (len(lines) * 42) + 40 849 + } 850 + 818 851 if quote != "" { 819 852 if len(quote) > 100 { 820 853 quote = quote[:97] + "..." ··· 833 866 drawText(img, "\""+line+"\"", padding+24, yPos+28+(i*32), textTertiary, 24, true) 834 867 } 835 868 yPos += 30 + (numLines * 32) + 30 836 - } 837 - 838 - if text != "" { 839 - if len(text) > 300 { 840 - text = text[:297] + "..." 841 - } 842 - lines := wrapTextToWidth(text, contentWidth, 32) 843 - for i, line := range lines { 844 - if i >= 6 { 845 - break 846 - } 847 - drawText(img, line, padding, yPos+(i*42), textPrimary, 32, false) 848 - } 849 869 } 850 870 851 871 drawText(img, source, padding, 580, textTertiary, 20, false) ··· 1004 1024 } 1005 1025 return lines 1006 1026 } 1027 + 1028 + func generateCollectionOGImagePNG(author, collectionName, description, icon, avatarURL string) image.Image { 1029 + width := 1200 1030 + height := 630 1031 + padding := 120 1032 + 1033 + bgPrimary := color.RGBA{12, 10, 20, 255} 1034 + accent := color.RGBA{168, 85, 247, 255} 1035 + textPrimary := color.RGBA{244, 240, 255, 255} 1036 + textSecondary := color.RGBA{168, 158, 200, 255} 1037 + textTertiary := color.RGBA{107, 95, 138, 255} 1038 + border := color.RGBA{45, 38, 64, 255} 1039 + 1040 + img := image.NewRGBA(image.Rect(0, 0, width, height)) 1041 + 1042 + draw.Draw(img, img.Bounds(), &image.Uniform{bgPrimary}, image.Point{}, draw.Src) 1043 + draw.Draw(img, image.Rect(0, 0, width, 12), &image.Uniform{accent}, image.Point{}, draw.Src) 1044 + 1045 + iconY := 120 1046 + var iconWidth int 1047 + if icon != "" { 1048 + emojiImg := fetchTwemojiImage(icon) 1049 + if emojiImg != nil { 1050 + iconSize := 96 1051 + drawScaledImage(img, emojiImg, padding, iconY, iconSize, iconSize) 1052 + iconWidth = iconSize + 32 1053 + } else { 1054 + drawText(img, icon, padding, iconY+70, textPrimary, 80, true) 1055 + iconWidth = 100 1056 + } 1057 + } 1058 + 1059 + drawText(img, collectionName, padding+iconWidth, iconY+65, textPrimary, 64, true) 1060 + 1061 + yPos := 280 1062 + contentWidth := width - (padding * 2) 1063 + 1064 + if description != "" { 1065 + if len(description) > 200 { 1066 + description = description[:197] + "..." 1067 + } 1068 + lines := wrapTextToWidth(description, contentWidth, 32) 1069 + for i, line := range lines { 1070 + if i >= 4 { 1071 + break 1072 + } 1073 + drawText(img, line, padding, yPos+(i*42), textSecondary, 32, false) 1074 + } 1075 + } else { 1076 + drawText(img, "A collection on Margin", padding, yPos, textTertiary, 32, false) 1077 + } 1078 + 1079 + yPos = 480 1080 + draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src) 1081 + 1082 + avatarSize := 64 1083 + avatarX := padding 1084 + avatarY := yPos + 40 1085 + 1086 + avatarImg := fetchAvatarImage(avatarURL) 1087 + if avatarImg != nil { 1088 + drawCircularAvatar(img, avatarImg, avatarX, avatarY, avatarSize) 1089 + } else { 1090 + drawDefaultAvatar(img, author, avatarX, avatarY, avatarSize, accent) 1091 + } 1092 + 1093 + handleX := avatarX + avatarSize + 24 1094 + drawText(img, author, handleX, avatarY+42, textTertiary, 28, false) 1095 + 1096 + return img 1097 + } 1098 + 1099 + func fetchTwemojiImage(emoji string) image.Image { 1100 + var codes []string 1101 + for _, r := range emoji { 1102 + codes = append(codes, fmt.Sprintf("%x", r)) 1103 + } 1104 + hexCode := strings.Join(codes, "-") 1105 + 1106 + url := fmt.Sprintf("https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/%s.png", hexCode) 1107 + 1108 + resp, err := http.Get(url) 1109 + if err != nil || resp.StatusCode != 200 { 1110 + if strings.Contains(hexCode, "-fe0f") { 1111 + simpleHex := strings.ReplaceAll(hexCode, "-fe0f", "") 1112 + url = fmt.Sprintf("https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/%s.png", simpleHex) 1113 + resp, err = http.Get(url) 1114 + if err != nil || resp.StatusCode != 200 { 1115 + return nil 1116 + } 1117 + } else { 1118 + return nil 1119 + } 1120 + } 1121 + defer resp.Body.Close() 1122 + 1123 + img, _, err := image.Decode(resp.Body) 1124 + if err != nil { 1125 + return nil 1126 + } 1127 + return img 1128 + } 1129 + 1130 + func generateHighlightOGImagePNG(author, pageTitle, quote, source, avatarURL string) image.Image { 1131 + width := 1200 1132 + height := 630 1133 + padding := 100 1134 + 1135 + bgPrimary := color.RGBA{12, 10, 20, 255} 1136 + accent := color.RGBA{250, 204, 21, 255} 1137 + textPrimary := color.RGBA{244, 240, 255, 255} 1138 + textSecondary := color.RGBA{168, 158, 200, 255} 1139 + border := color.RGBA{45, 38, 64, 255} 1140 + 1141 + img := image.NewRGBA(image.Rect(0, 0, width, height)) 1142 + 1143 + draw.Draw(img, img.Bounds(), &image.Uniform{bgPrimary}, image.Point{}, draw.Src) 1144 + draw.Draw(img, image.Rect(0, 0, width, 12), &image.Uniform{accent}, image.Point{}, draw.Src) 1145 + 1146 + avatarSize := 64 1147 + avatarX := padding 1148 + avatarY := padding 1149 + 1150 + avatarImg := fetchAvatarImage(avatarURL) 1151 + if avatarImg != nil { 1152 + drawCircularAvatar(img, avatarImg, avatarX, avatarY, avatarSize) 1153 + } else { 1154 + drawDefaultAvatar(img, author, avatarX, avatarY, avatarSize, accent) 1155 + } 1156 + drawText(img, author, avatarX+avatarSize+24, avatarY+42, textSecondary, 28, false) 1157 + 1158 + contentWidth := width - (padding * 2) 1159 + yPos := 240 1160 + 1161 + if quote != "" { 1162 + if len(quote) > 200 { 1163 + quote = quote[:197] + "..." 1164 + } 1165 + 1166 + barHeight := 0 1167 + 1168 + lines := wrapTextToWidth(quote, contentWidth-40, 42) 1169 + barHeight = len(lines) * 56 1170 + 1171 + draw.Draw(img, image.Rect(padding, yPos, padding+8, yPos+barHeight), &image.Uniform{accent}, image.Point{}, draw.Src) 1172 + 1173 + for i, line := range lines { 1174 + if i >= 5 { 1175 + break 1176 + } 1177 + drawText(img, line, padding+40, yPos+42+(i*56), textPrimary, 42, false) 1178 + } 1179 + yPos += barHeight + 60 1180 + } 1181 + 1182 + draw.Draw(img, image.Rect(padding, yPos, width-padding, yPos+1), &image.Uniform{border}, image.Point{}, draw.Src) 1183 + yPos += 40 1184 + 1185 + if pageTitle != "" { 1186 + if len(pageTitle) > 60 { 1187 + pageTitle = pageTitle[:57] + "..." 1188 + } 1189 + drawText(img, pageTitle, padding, yPos+32, textSecondary, 32, true) 1190 + } 1191 + 1192 + if source != "" { 1193 + drawText(img, source, padding, yPos+80, textSecondary, 24, false) 1194 + } 1195 + 1196 + return img 1197 + }