appview/repo: construct and serve og image for repos #648

merged
opened by anirudh.fi targeting master from push-vyusnwqnmxwy
Changed files
+449 -3
appview
pages
templates
fragments
repo
+3 -3
appview/pages/funcmap.go
··· 283 283 }, 284 284 285 285 "tinyAvatar": func(handle string) string { 286 - return p.avatarUri(handle, "tiny") 286 + return p.AvatarUrl(handle, "tiny") 287 287 }, 288 288 "fullAvatar": func(handle string) string { 289 - return p.avatarUri(handle, "") 289 + return p.AvatarUrl(handle, "") 290 290 }, 291 291 "langColor": enry.GetColor, 292 292 "layoutSide": func() string { ··· 310 310 } 311 311 } 312 312 313 - func (p *Pages) avatarUri(handle, size string) string { 313 + func (p *Pages) AvatarUrl(handle, size string) string { 314 314 handle = strings.TrimPrefix(handle, "@") 315 315 316 316 secret := p.avatar.SharedSecret
+44
appview/pages/templates/fragments/dolly/silhouette.svg
··· 1 + <svg 2 + version="1.1" 3 + id="svg1" 4 + width="32" 5 + height="32" 6 + viewBox="0 0 25 25" 7 + sodipodi:docname="tangled_dolly_silhouette.png" 8 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 9 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 10 + xmlns="http://www.w3.org/2000/svg" 11 + xmlns:svg="http://www.w3.org/2000/svg"> 12 + <title>Dolly</title> 13 + <defs 14 + id="defs1" /> 15 + <sodipodi:namedview 16 + id="namedview1" 17 + pagecolor="#ffffff" 18 + bordercolor="#000000" 19 + borderopacity="0.25" 20 + inkscape:showpageshadow="2" 21 + inkscape:pageopacity="0.0" 22 + inkscape:pagecheckerboard="true" 23 + inkscape:deskcolor="#d1d1d1"> 24 + <inkscape:page 25 + x="0" 26 + y="0" 27 + width="25" 28 + height="25" 29 + id="page2" 30 + margin="0" 31 + bleed="0" /> 32 + </sodipodi:namedview> 33 + <g 34 + inkscape:groupmode="layer" 35 + inkscape:label="Image" 36 + id="g1"> 37 + <path 38 + class="dolly" 39 + fill="currentColor" 40 + style="stroke-width:1.12248" 41 + d="m 16.208435,23.914069 c -0.06147,-0.02273 -0.147027,-0.03034 -0.190158,-0.01691 -0.197279,0.06145 -1.31068,-0.230493 -1.388819,-0.364153 -0.01956,-0.03344 -0.163274,-0.134049 -0.319377,-0.223561 -0.550395,-0.315603 -1.010951,-0.696643 -1.428383,-1.181771 -0.264598,-0.307509 -0.597257,-0.785384 -0.597257,-0.857979 0,-0.0216 -0.02841,-0.06243 -0.06313,-0.0907 -0.04977,-0.04053 -0.160873,0.0436 -0.52488,0.397463 -0.479803,0.466432 -0.78924,0.689475 -1.355603,0.977118 -0.183693,0.0933 -0.323426,0.179989 -0.310516,0.192658 0.02801,0.02748 -0.7656391,0.270031 -1.209129,0.369517 -0.5378332,0.120647 -1.6341809,0.08626 -1.9721503,-0.06186 C 6.7977157,23.031391 6.56735,22.957551 6.3371134,22.889782 4.9717169,22.487902 3.7511914,21.481518 3.1172396,20.234838 2.6890391,19.392772 2.5582276,18.827446 2.5610489,17.831154 2.5639589,16.802192 2.7366641,16.125844 3.2142117,15.273187 3.3040457,15.112788 3.3713143,14.976533 3.3636956,14.9704 3.3560756,14.9643 3.2459634,14.90305 3.1189994,14.834381 1.7582586,14.098312 0.77760984,12.777439 0.44909837,11.23818 0.33531456,10.705039 0.33670119,9.7067968 0.45195381,9.1778795 0.72259241,7.9359287 1.3827188,6.8888436 2.4297498,6.0407205 2.6856126,5.8334648 3.2975489,5.4910878 3.6885849,5.3364049 L 4.0584319,5.190106 4.2333984,4.860432 C 4.8393906,3.7186139 5.8908314,2.7968028 7.1056396,2.3423025 7.7690673,2.0940921 8.2290216,2.0150935 9.01853,2.0137575 c 0.9625627,-0.00163 1.629181,0.1532762 2.485864,0.5776514 l 0.271744,0.1346134 0.42911,-0.3607688 c 1.082666,-0.9102346 2.185531,-1.3136811 3.578383,-1.3090327 0.916696,0.00306 1.573918,0.1517893 2.356121,0.5331927 1.465948,0.7148 2.54506,2.0625628 2.865177,3.57848 l 0.07653,0.362429 0.515095,0.2556611 c 1.022872,0.5076874 1.756122,1.1690944 2.288361,2.0641468 0.401896,0.6758594 0.537303,1.0442682 0.675505,1.8378683 0.288575,1.6570823 -0.266229,3.3548023 -1.490464,4.5608743 -0.371074,0.36557 -0.840205,0.718265 -1.203442,0.904754 -0.144112,0.07398 -0.271303,0.15826 -0.282647,0.187269 -0.01134,0.02901 0.02121,0.142764 0.07234,0.25279 0.184248,0.396467 0.451371,1.331823 0.619371,2.168779 0.463493,2.30908 -0.754646,4.693707 -2.92278,5.721632 -0.479538,0.227352 -0.717629,0.309322 -1.144194,0.39393 -0.321869,0.06383 -1.850573,0.09139 -2.000174,0.03604 z M 12.25443,18.636956 c 0.739923,-0.24652 1.382521,-0.718922 1.874623,-1.37812 0.0752,-0.100718 0.213883,-0.275851 0.308198,-0.389167 0.09432,-0.113318 0.210136,-0.271056 0.257381,-0.350531 0.416347,-0.700389 0.680936,-1.176102 0.766454,-1.378041 0.05594,-0.132087 0.114653,-0.239607 0.130477,-0.238929 0.01583,6.79e-4 0.08126,0.08531 0.145412,0.188069 0.178029,0.285173 0.614305,0.658998 0.868158,0.743878 0.259802,0.08686 0.656158,0.09598 0.911369,0.02095 0.213812,-0.06285 0.507296,-0.298016 0.645179,-0.516947 0.155165,-0.246374 0.327989,-0.989595 0.327989,-1.410501 0,-1.26718 -0.610975,-3.143405 -1.237774,-3.801045 -0.198483,-0.2082486 -0.208557,-0.2319396 -0.208557,-0.4904655 0,-0.2517771 -0.08774,-0.5704927 -0.258476,-0.938956 C 16.694963,8.50313 16.375697,8.1377479 16.135846,7.9543702 L 15.932296,7.7987471 15.683004,7.9356529 C 15.131767,8.2383821 14.435638,8.1945733 13.943459,7.8261812 L 13.782862,7.7059758 13.686773,7.8908012 C 13.338849,8.5600578 12.487087,8.8811064 11.743178,8.6233891 11.487199,8.5347109 11.358897,8.4505994 11.063189,8.1776138 L 10.69871,7.8411436 10.453484,8.0579255 C 10.318608,8.1771557 10.113778,8.3156283 9.9983037,8.3656417 9.7041488,8.4930449 9.1808299,8.5227884 8.8979004,8.4281886 8.7754792,8.3872574 8.6687415,8.3537661 8.6607053,8.3537661 c -0.03426,0 -0.3092864,0.3066098 -0.3791974,0.42275 -0.041935,0.069664 -0.1040482,0.1266636 -0.1380294,0.1266636 -0.1316419,0 -0.4197402,0.1843928 -0.6257041,0.4004735 -0.1923125,0.2017571 -0.6853701,0.9036038 -0.8926582,1.2706578 -0.042662,0.07554 -0.1803555,0.353687 -0.3059848,0.618091 -0.1256293,0.264406 -0.3270073,0.686768 -0.4475067,0.938581 -0.1204992,0.251816 -0.2469926,0.519654 -0.2810961,0.595199 -0.2592829,0.574347 -0.285919,1.391094 -0.057822,1.77304 0.1690683,0.283105 0.4224039,0.480895 0.7285507,0.568809 0.487122,0.139885 0.9109638,-0.004 1.6013422,-0.543768 l 0.4560939,-0.356568 0.0036,0.172041 c 0.01635,0.781837 0.1831084,1.813183 0.4016641,2.484154 0.1160449,0.356262 0.3781448,0.83968 0.5614081,1.035462 0.2171883,0.232025 0.7140951,0.577268 1.0100284,0.701749 0.121485,0.0511 0.351032,0.110795 0.510105,0.132647 0.396966,0.05452 1.2105,0.02265 1.448934,-0.05679 z" 42 + id="path1" /> 43 + </g> 44 + </svg>
+393
appview/repo/opengraph.go
··· 1 + package repo 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/hex" 7 + "encoding/json" 8 + "fmt" 9 + "image/color" 10 + "image/png" 11 + "log" 12 + "net/http" 13 + "strings" 14 + 15 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 + "tangled.org/core/api/tangled" 17 + "tangled.org/core/appview/models" 18 + "tangled.org/core/appview/repo/ogcard" 19 + "tangled.org/core/types" 20 + ) 21 + 22 + func (rp *Repo) drawRepoSummaryCard(repo *models.Repo, languageStats []types.RepoLanguageDetails) (*ogcard.Card, error) { 23 + width, height := ogcard.DefaultSize() 24 + mainCard, err := ogcard.NewCard(width, height) 25 + if err != nil { 26 + return nil, err 27 + } 28 + 29 + // Split: content area (75%) and language bar + icons (25%) 30 + contentCard, bottomArea := mainCard.Split(false, 75) 31 + 32 + // Add padding to content 33 + contentCard.SetMargin(30) 34 + 35 + // Split content horizontally: main content (80%) and avatar area (20%) 36 + mainContent, avatarArea := contentCard.Split(true, 80) 37 + 38 + // Split main content: 50% for name/description, 50% for spacing 39 + topSection, _ := mainContent.Split(false, 50) 40 + 41 + // Split top section: 40% for repo name, 60% for description 42 + repoNameCard, descriptionCard := topSection.Split(false, 50) 43 + 44 + // Draw repo name with owner in regular and repo name in bold 45 + repoNameCard.SetMargin(10) 46 + var ownerHandle string 47 + owner, err := rp.idResolver.ResolveIdent(context.Background(), repo.Did) 48 + if err != nil { 49 + ownerHandle = repo.Did 50 + } else { 51 + ownerHandle = "@" + owner.Handle.String() 52 + } 53 + 54 + // Draw repo name with wrapping support 55 + repoNameCard.SetMargin(10) 56 + bounds := repoNameCard.Img.Bounds() 57 + startX := bounds.Min.X + repoNameCard.Margin 58 + startY := bounds.Min.Y + repoNameCard.Margin 59 + currentX := startX 60 + textColor := color.RGBA{88, 96, 105, 255} 61 + 62 + // Draw owner handle in gray 63 + ownerWidth, err := repoNameCard.DrawTextAtWithWidth(ownerHandle, currentX, startY, textColor, 54, ogcard.Top, ogcard.Left) 64 + if err != nil { 65 + return nil, err 66 + } 67 + currentX += ownerWidth 68 + 69 + // Draw separator 70 + sepWidth, err := repoNameCard.DrawTextAtWithWidth(" / ", currentX, startY, textColor, 54, ogcard.Top, ogcard.Left) 71 + if err != nil { 72 + return nil, err 73 + } 74 + currentX += sepWidth 75 + 76 + // Draw repo name in bold 77 + _, err = repoNameCard.DrawBoldText(repo.Name, currentX, startY, color.Black, 54, ogcard.Top, ogcard.Left) 78 + if err != nil { 79 + return nil, err 80 + } 81 + 82 + // Draw description (DrawText handles multi-line wrapping automatically) 83 + descriptionCard.SetMargin(10) 84 + description := repo.Description 85 + if len(description) > 80 { 86 + description = description[:100] + "…" 87 + } 88 + 89 + _, err = descriptionCard.DrawText(description, color.RGBA{88, 96, 105, 255}, 36, ogcard.Top, ogcard.Left) 90 + if err != nil { 91 + log.Printf("failed to draw description: %v", err) 92 + return nil, err 93 + } 94 + 95 + // Draw avatar circle on the right side 96 + avatarBounds := avatarArea.Img.Bounds() 97 + avatarSize := min(avatarBounds.Dx(), avatarBounds.Dy()) - 20 // Leave some margin 98 + if avatarSize > 220 { 99 + avatarSize = 220 100 + } 101 + avatarX := avatarBounds.Min.X + (avatarBounds.Dx() / 2) - (avatarSize / 2) 102 + avatarY := avatarBounds.Min.Y + 20 103 + 104 + // Get avatar URL and draw it 105 + avatarURL := rp.pages.AvatarUrl(ownerHandle, "256") 106 + log.Printf("Fetching avatar from: %s", avatarURL) 107 + err = avatarArea.DrawCircularExternalImage(avatarURL, avatarX, avatarY, avatarSize) 108 + if err != nil { 109 + log.Printf("failed to draw avatar (non-fatal): %v", err) 110 + } 111 + 112 + // Split bottom area: icons area (65%) and language bar (35%) 113 + iconsArea, languageBarCard := bottomArea.Split(false, 75) 114 + 115 + // Split icons area: left side for stats (85%), right side for dolly (15%) 116 + statsArea, dollyArea := iconsArea.Split(true, 85) 117 + 118 + // Draw stats with icons in the stats area 119 + starsText := repo.RepoStats.StarCount 120 + issuesText := repo.RepoStats.IssueCount.Open 121 + pullRequestsText := repo.RepoStats.PullCount.Open 122 + 123 + iconColor := color.RGBA{88, 96, 105, 255} 124 + iconSize := 36 125 + textSize := 36.0 126 + 127 + // Position stats in the middle of the stats area 128 + statsBounds := statsArea.Img.Bounds() 129 + log.Printf("Stats area bounds: %v", statsBounds) 130 + statsX := statsBounds.Min.X + 60 // left padding 131 + statsY := statsBounds.Min.Y 132 + currentX = statsX 133 + labelSize := 22.0 134 + log.Printf("Stats starting position: X=%d, Y=%d", statsX, statsY) 135 + // Draw star icon, count, and label 136 + // Align icon baseline with text baseline 137 + iconBaselineOffset := int(textSize) / 2 138 + err = statsArea.DrawSVGIcon("static/icons/star.svg", currentX, statsY+iconBaselineOffset-iconSize/2, iconSize, iconColor) 139 + if err != nil { 140 + log.Printf("failed to draw star icon: %v", err) 141 + } 142 + starIconX := currentX 143 + currentX += iconSize + 15 144 + 145 + starText := fmt.Sprintf("%d", starsText) 146 + err = statsArea.DrawTextAt(starText, currentX, statsY+iconBaselineOffset, iconColor, textSize, ogcard.Middle, ogcard.Left) 147 + if err != nil { 148 + log.Printf("failed to draw star text: %v", err) 149 + } 150 + starTextWidth := len(starText) * 20 151 + starGroupWidth := iconSize + 15 + starTextWidth 152 + 153 + // Draw "stars" label below and centered under the icon+text group 154 + labelY := statsY + iconSize + 15 155 + labelX := starIconX + starGroupWidth/2 156 + log.Printf("Drawing 'stars' label at X=%d, Y=%d, bounds: %v", labelX, labelY, statsBounds) 157 + err = iconsArea.DrawTextAt("stars", labelX, labelY, iconColor, labelSize, ogcard.Top, ogcard.Center) 158 + if err != nil { 159 + log.Printf("failed to draw stars label: %v", err) 160 + } else { 161 + log.Printf("Successfully drew 'stars' label") 162 + } 163 + 164 + currentX += starTextWidth + 50 165 + 166 + // Draw issues icon, count, and label 167 + issueStartX := currentX 168 + err = statsArea.DrawSVGIcon("static/icons/circle-dot.svg", currentX, statsY+iconBaselineOffset-iconSize/2, iconSize, iconColor) 169 + if err != nil { 170 + log.Printf("failed to draw circle-dot icon: %v", err) 171 + } 172 + currentX += iconSize + 15 173 + 174 + issueText := fmt.Sprintf("%d", issuesText) 175 + err = statsArea.DrawTextAt(issueText, currentX, statsY+iconBaselineOffset, iconColor, textSize, ogcard.Middle, ogcard.Left) 176 + if err != nil { 177 + log.Printf("failed to draw issue text: %v", err) 178 + } 179 + issueTextWidth := len(issueText) * 20 180 + issueGroupWidth := iconSize + 15 + issueTextWidth 181 + 182 + // Draw "issues" label below and centered under the icon+text group 183 + labelX = issueStartX + issueGroupWidth/2 184 + log.Printf("Drawing 'issues' label at X=%d, Y=%d", labelX, labelY) 185 + err = iconsArea.DrawTextAt("issues", labelX, labelY, iconColor, labelSize, ogcard.Top, ogcard.Center) 186 + if err != nil { 187 + log.Printf("failed to draw issues label: %v", err) 188 + } else { 189 + log.Printf("Successfully drew 'issues' label") 190 + } 191 + 192 + currentX += issueTextWidth + 50 193 + 194 + // Draw pull request icon, count, and label 195 + prStartX := currentX 196 + err = statsArea.DrawSVGIcon("static/icons/git-pull-request.svg", currentX, statsY+iconBaselineOffset-iconSize/2, iconSize, iconColor) 197 + if err != nil { 198 + log.Printf("failed to draw git-pull-request icon: %v", err) 199 + } 200 + currentX += iconSize + 15 201 + 202 + prText := fmt.Sprintf("%d", pullRequestsText) 203 + err = statsArea.DrawTextAt(prText, currentX, statsY+iconBaselineOffset, iconColor, textSize, ogcard.Middle, ogcard.Left) 204 + if err != nil { 205 + log.Printf("failed to draw PR text: %v", err) 206 + } 207 + prTextWidth := len(prText) * 20 208 + prGroupWidth := iconSize + 15 + prTextWidth 209 + 210 + // Draw "pulls" label below and centered under the icon+text group 211 + labelX = prStartX + prGroupWidth/2 212 + log.Printf("Drawing 'pulls' label at X=%d, Y=%d", labelX, labelY) 213 + err = iconsArea.DrawTextAt("pulls", labelX, labelY, iconColor, labelSize, ogcard.Top, ogcard.Center) 214 + if err != nil { 215 + log.Printf("failed to draw pulls label: %v", err) 216 + } else { 217 + log.Printf("Successfully drew 'pulls' label") 218 + } 219 + 220 + dollyBounds := dollyArea.Img.Bounds() 221 + dollySize := 90 222 + dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 223 + dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 224 + dollyColor := color.RGBA{180, 180, 180, 255} // light gray 225 + err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 226 + if err != nil { 227 + log.Printf("dolly silhouette not available (this is ok): %v", err) 228 + } 229 + 230 + // Draw language bar at bottom 231 + err = drawLanguagesCard(languageBarCard, languageStats) 232 + if err != nil { 233 + log.Printf("failed to draw language bar: %v", err) 234 + return nil, err 235 + } 236 + 237 + return mainCard, nil 238 + } 239 + 240 + // hexToColor converts a hex color to a go color 241 + func hexToColor(colorStr string) (*color.RGBA, error) { 242 + colorStr = strings.TrimLeft(colorStr, "#") 243 + 244 + b, err := hex.DecodeString(colorStr) 245 + if err != nil { 246 + return nil, err 247 + } 248 + 249 + if len(b) < 3 { 250 + return nil, fmt.Errorf("expected at least 3 bytes from DecodeString, got %d", len(b)) 251 + } 252 + 253 + clr := color.RGBA{b[0], b[1], b[2], 255} 254 + 255 + return &clr, nil 256 + } 257 + 258 + func drawLanguagesCard(card *ogcard.Card, languageStats []types.RepoLanguageDetails) error { 259 + bounds := card.Img.Bounds() 260 + cardWidth := bounds.Dx() 261 + 262 + if len(languageStats) == 0 { 263 + // Draw a light gray bar if no languages detected 264 + card.DrawRect(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, color.RGBA{225, 228, 232, 255}) 265 + return nil 266 + } 267 + 268 + // Limit to top 5 languages for the visual bar 269 + displayLanguages := languageStats 270 + if len(displayLanguages) > 5 { 271 + displayLanguages = displayLanguages[:5] 272 + } 273 + 274 + currentX := bounds.Min.X 275 + 276 + for _, lang := range displayLanguages { 277 + var langColor *color.RGBA 278 + var err error 279 + 280 + if lang.Color != "" { 281 + langColor, err = hexToColor(lang.Color) 282 + if err != nil { 283 + // Fallback to a default color 284 + langColor = &color.RGBA{149, 157, 165, 255} 285 + } 286 + } else { 287 + // Default color if no color specified 288 + langColor = &color.RGBA{149, 157, 165, 255} 289 + } 290 + 291 + langWidth := float32(cardWidth) * (lang.Percentage / 100) 292 + card.DrawRect(currentX, bounds.Min.Y, currentX+int(langWidth), bounds.Max.Y, langColor) 293 + currentX += int(langWidth) 294 + } 295 + 296 + // Fill remaining space with the last color (if any gap due to rounding) 297 + if currentX < bounds.Max.X && len(displayLanguages) > 0 { 298 + lastLang := displayLanguages[len(displayLanguages)-1] 299 + var lastColor *color.RGBA 300 + var err error 301 + 302 + if lastLang.Color != "" { 303 + lastColor, err = hexToColor(lastLang.Color) 304 + if err != nil { 305 + lastColor = &color.RGBA{149, 157, 165, 255} 306 + } 307 + } else { 308 + lastColor = &color.RGBA{149, 157, 165, 255} 309 + } 310 + card.DrawRect(currentX, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, lastColor) 311 + } 312 + 313 + return nil 314 + } 315 + 316 + func (rp *Repo) RepoOpenGraphSummary(w http.ResponseWriter, r *http.Request) { 317 + f, err := rp.repoResolver.Resolve(r) 318 + if err != nil { 319 + log.Println("failed to get repo and knot", err) 320 + return 321 + } 322 + 323 + // Get language stats 324 + scheme := "http" 325 + if !rp.config.Core.Dev { 326 + scheme = "https" 327 + } 328 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 329 + xrpcc := &indigoxrpc.Client{ 330 + Host: host, 331 + } 332 + 333 + // Get default branch/ref for language stats 334 + var languageStats []types.RepoLanguageDetails 335 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 336 + 337 + // Fetch branches to get default branch 338 + branchesBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 339 + if err == nil { 340 + var branchesResp types.RepoBranchesResponse 341 + if err := json.Unmarshal(branchesBytes, &branchesResp); err == nil { 342 + // Find default branch 343 + var defaultRef string 344 + for _, branch := range branchesResp.Branches { 345 + if branch.IsDefault { 346 + defaultRef = branch.Name 347 + break 348 + } 349 + } 350 + 351 + // If no default branch, use first branch 352 + if defaultRef == "" && len(branchesResp.Branches) > 0 { 353 + defaultRef = branchesResp.Branches[0].Name 354 + } 355 + 356 + if defaultRef != "" { 357 + langStats, err := rp.getLanguageInfo(r.Context(), f, xrpcc, defaultRef, true) 358 + if err != nil { 359 + log.Println("failed to get language stats for opengraph", err) 360 + // non-fatal, continue without language stats 361 + } else { 362 + languageStats = langStats 363 + } 364 + } 365 + } 366 + } 367 + 368 + card, err := rp.drawRepoSummaryCard(&f.Repo, languageStats) 369 + if err != nil { 370 + log.Println("failed to draw repo summary card", err) 371 + http.Error(w, "failed to draw repo summary card", http.StatusInternalServerError) 372 + return 373 + } 374 + 375 + var imageBuffer bytes.Buffer 376 + err = png.Encode(&imageBuffer, card.Img) 377 + if err != nil { 378 + log.Println("failed to encode repo summary card", err) 379 + http.Error(w, "failed to encode repo summary card", http.StatusInternalServerError) 380 + return 381 + } 382 + 383 + imageBytes := imageBuffer.Bytes() 384 + 385 + w.Header().Set("Content-Type", "image/png") 386 + w.Header().Set("Cache-Control", "public, max-age=3600") // 1 hour 387 + w.WriteHeader(http.StatusOK) 388 + _, err = w.Write(imageBytes) 389 + if err != nil { 390 + log.Println("failed to write repo summary card", err) 391 + return 392 + } 393 + }
+1
appview/repo/router.go
··· 10 10 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 11 r := chi.NewRouter() 12 12 r.Get("/", rp.RepoIndex) 13 + r.Get("/opengraph", rp.RepoOpenGraphSummary) 13 14 r.Get("/feed.atom", rp.RepoAtomFeed) 14 15 r.Get("/commits/{ref}", rp.RepoLog) 15 16 r.Route("/tree/{ref}", func(r chi.Router) {
+2
go.mod
··· 37 37 github.com/redis/go-redis/v9 v9.7.3 38 38 github.com/resend/resend-go/v2 v2.15.0 39 39 github.com/sethvargo/go-envconfig v1.1.0 40 + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c 41 + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef 40 42 github.com/stretchr/testify v1.10.0 41 43 github.com/urfave/cli/v3 v3.3.3 42 44 github.com/whyrusleeping/cbor-gen v0.3.1
+6
go.sum
··· 136 136 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 137 137 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 138 138 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 139 + github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A= 140 + github.com/goki/freetype v1.0.5/go.mod h1:wKmKxddbzKmeci9K96Wknn5kjTWLyfC8tKOqAFbEX8E= 139 141 github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= 140 142 github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 141 143 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= ··· 397 399 github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 398 400 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 399 401 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 402 + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= 403 + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= 404 + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= 405 + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= 400 406 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 401 407 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 402 408 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=