Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

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

feat: more opengraph tweaks

+68 -31
+7 -6
internal/handlers/brew.go
··· 28 28 return 29 29 } 30 30 31 - var ogTitle string 31 + var subtitle string 32 32 if brew.Bean != nil { 33 - ogTitle = brew.Bean.Name 34 - } else { 35 - ogTitle = "Coffee Brew" 33 + subtitle = brew.Bean.Name 34 + if brew.Bean.Roaster != nil && brew.Bean.Roaster.Name != "" { 35 + subtitle += " from " + brew.Bean.Roaster.Name 36 + } 36 37 } 37 38 38 - populateOGFields(layoutData, ogTitle, "brew", owner, baseURL, shareURL) 39 + populateOGFields(layoutData, subtitle, "brew", owner, baseURL, shareURL) 39 40 } 40 41 41 42 // HandleBrewOGImage generates a 1200x630 PNG preview card for a brew. ··· 300 301 301 302 // Create layout data with OpenGraph metadata 302 303 layoutData := h.buildLayoutData(r, "Brew Details", isAuthenticated, didStr, userProfile) 303 - h.populateBrewOGMetadata(layoutData, brew, owner, h.publicBaseURL(r), shareURL) 304 + h.populateBrewOGMetadata(layoutData, brew, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 304 305 305 306 // Get like data 306 307 var isLiked bool
+12 -9
internal/handlers/entity_views.go
··· 206 206 } 207 207 208 208 layoutData := h.buildLayoutData(r, beanViewProps.Bean.Name, isAuthenticated, didStr, userProfile) 209 - h.populateBeanOGMetadata(layoutData, beanViewProps.Bean, owner, h.publicBaseURL(r), shareURL) 209 + h.populateBeanOGMetadata(layoutData, beanViewProps.Bean, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 210 210 211 211 sd := h.fetchSocialData(r.Context(), subjectURI, didStr, isAuthenticated) 212 212 ··· 338 338 } 339 339 340 340 layoutData := h.buildLayoutData(r, props.Roaster.Name, isAuthenticated, didStr, userProfile) 341 - h.populateRoasterOGMetadata(layoutData, props.Roaster, owner, h.publicBaseURL(r), shareURL) 341 + h.populateRoasterOGMetadata(layoutData, props.Roaster, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 342 342 343 343 sd := h.fetchSocialData(r.Context(), subjectURI, didStr, isAuthenticated) 344 344 ··· 470 470 } 471 471 472 472 layoutData := h.buildLayoutData(r, props.Grinder.Name, isAuthenticated, didStr, userProfile) 473 - h.populateGrinderOGMetadata(layoutData, props.Grinder, owner, h.publicBaseURL(r), shareURL) 473 + h.populateGrinderOGMetadata(layoutData, props.Grinder, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 474 474 475 475 sd := h.fetchSocialData(r.Context(), subjectURI, didStr, isAuthenticated) 476 476 ··· 602 602 } 603 603 604 604 layoutData := h.buildLayoutData(r, props.Brewer.Name, isAuthenticated, didStr, userProfile) 605 - h.populateBrewerOGMetadata(layoutData, props.Brewer, owner, h.publicBaseURL(r), shareURL) 605 + h.populateBrewerOGMetadata(layoutData, props.Brewer, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 606 606 607 607 sd := h.fetchSocialData(r.Context(), subjectURI, didStr, isAuthenticated) 608 608 ··· 769 769 } 770 770 771 771 layoutData := h.buildLayoutData(r, props.Recipe.Name, isAuthenticated, didStr, userProfile) 772 - h.populateRecipeOGMetadata(layoutData, props.Recipe, owner, h.publicBaseURL(r), shareURL) 772 + h.populateRecipeOGMetadata(layoutData, props.Recipe, h.resolveOwnerHandle(r.Context(), owner), h.publicBaseURL(r), shareURL) 773 773 774 774 sd := h.fetchSocialData(r.Context(), subjectURI, didStr, isAuthenticated) 775 775 ··· 1147 1147 if bean == nil { 1148 1148 return 1149 1149 } 1150 - title := bean.Name 1151 - if title == "" { 1152 - title = bean.Origin 1150 + subtitle := bean.Name 1151 + if subtitle == "" { 1152 + subtitle = bean.Origin 1153 1153 } 1154 - populateOGFields(layoutData, title, "bean", owner, baseURL, shareURL) 1154 + if bean.Roaster != nil && bean.Roaster.Name != "" { 1155 + subtitle += " from " + bean.Roaster.Name 1156 + } 1157 + populateOGFields(layoutData, subtitle, "bean", owner, baseURL, shareURL) 1155 1158 } 1156 1159 1157 1160 func (h *Handler) populateRoasterOGMetadata(layoutData *components.LayoutData, roaster *models.Roaster, owner, baseURL, shareURL string) {
+22 -5
internal/handlers/handlers.go
··· 342 342 w.WriteHeader(http.StatusOK) 343 343 } 344 344 345 + // resolveOwnerHandle returns a human-readable handle for the owner string. 346 + // If the owner is already a handle, it is returned as-is. If it is a DID, 347 + // the feed index profile cache is consulted to resolve it to a handle. 348 + func (h *Handler) resolveOwnerHandle(ctx context.Context, owner string) string { 349 + if !strings.HasPrefix(owner, "did:") { 350 + return owner 351 + } 352 + if h.feedIndex != nil { 353 + if profile, err := h.feedIndex.GetProfile(ctx, owner); err == nil && profile.Handle != "" { 354 + return profile.Handle 355 + } 356 + } 357 + return owner 358 + } 359 + 345 360 // populateOGFields sets the standard OG metadata fields for an entity page. 346 - // The description follows the pattern "{type} from {owner} on arabica.social". 347 - func populateOGFields(layoutData *components.LayoutData, title, recordType, owner, baseURL, shareURL string) { 348 - layoutData.OGTitle = title 361 + // The title follows the pattern "{type} from {owner} on arabica.social". 362 + // The subtitle (OG description) shows record-specific detail like the bean name. 363 + func populateOGFields(layoutData *components.LayoutData, subtitle, recordType, owner, baseURL, shareURL string) { 349 364 layoutData.OGType = "article" 350 365 351 366 if owner != "" { 352 - layoutData.OGDescription = fmt.Sprintf("%s from %s on arabica.social", recordType, owner) 367 + layoutData.OGTitle = fmt.Sprintf("%s from %s on arabica.social", recordType, owner) 353 368 } else { 354 - layoutData.OGDescription = fmt.Sprintf("%s on arabica.social", recordType) 369 + layoutData.OGTitle = fmt.Sprintf("%s on arabica.social", recordType) 355 370 } 371 + 372 + layoutData.OGDescription = subtitle 356 373 357 374 if baseURL != "" && shareURL != "" { 358 375 layoutData.OGUrl = baseURL + shareURL
+13 -10
internal/handlers/handlers_test.go
··· 467 467 publicURL: "https://arabica.example.com", 468 468 }, 469 469 { 470 - name: "brew with bean", 470 + name: "brew with bean and roaster", 471 471 brew: &models.Brew{ 472 - Bean: &models.Bean{Name: "Ethiopian Yirgacheffe"}, 472 + Bean: &models.Bean{ 473 + Name: "Ethiopian Yirgacheffe", 474 + Roaster: &models.Roaster{Name: "Sweet Maria's"}, 475 + }, 473 476 }, 474 477 owner: "alice.bsky.social", 475 478 shareURL: "/brews/123?owner=alice.bsky.social", 476 479 publicURL: "https://arabica.example.com", 477 - wantTitle: "Ethiopian Yirgacheffe", 478 - wantDescription: "brew from alice.bsky.social on arabica.social", 480 + wantTitle: "brew from alice.bsky.social on arabica.social", 481 + wantDescription: "Ethiopian Yirgacheffe from Sweet Maria's", 479 482 wantType: "article", 480 483 wantURL: "https://arabica.example.com/brews/123?owner=alice.bsky.social", 481 484 wantImage: "https://arabica.example.com/brews/123/og-image?owner=alice.bsky.social", ··· 486 489 owner: "bob.bsky.social", 487 490 shareURL: "/brews/789?owner=bob.bsky.social", 488 491 publicURL: "https://arabica.example.com", 489 - wantTitle: "Coffee Brew", 490 - wantDescription: "brew from bob.bsky.social on arabica.social", 492 + wantTitle: "brew from bob.bsky.social on arabica.social", 493 + wantDescription: "", 491 494 wantType: "article", 492 495 wantURL: "https://arabica.example.com/brews/789?owner=bob.bsky.social", 493 496 wantImage: "https://arabica.example.com/brews/789/og-image?owner=bob.bsky.social", ··· 500 503 owner: "alice.bsky.social", 501 504 shareURL: "/brews/xyz", 502 505 publicURL: "", 503 - wantTitle: "Premium Blend", 504 - wantDescription: "brew from alice.bsky.social on arabica.social", 506 + wantTitle: "brew from alice.bsky.social on arabica.social", 507 + wantDescription: "Premium Blend", 505 508 wantType: "article", 506 509 }, 507 510 { ··· 512 515 owner: "", 513 516 shareURL: "/brews/456", 514 517 publicURL: "https://arabica.example.com", 515 - wantTitle: "House Blend", 516 - wantDescription: "brew on arabica.social", 518 + wantTitle: "brew on arabica.social", 519 + wantDescription: "House Blend", 517 520 wantType: "article", 518 521 wantURL: "https://arabica.example.com/brews/456", 519 522 wantImage: "https://arabica.example.com/brews/456/og-image",
+14 -1
internal/web/components/layout.templ
··· 16 16 OGTitle string // Falls back to Title + " - Arabica" 17 17 OGDescription string // Falls back to site description 18 18 OGImage string // If set, renders og:image tag 19 + OGImageAlt string // Alt text for OG image; falls back to OGTitle + OGDescription 19 20 OGType string // Falls back to "website" 20 21 OGUrl string // Canonical URL for the page 21 22 } ··· 44 45 return "website" 45 46 } 46 47 48 + // ogImageAlt returns descriptive alt text for the OG image. 49 + func (d *LayoutData) ogImageAlt() string { 50 + if d.OGImageAlt != "" { 51 + return d.OGImageAlt 52 + } 53 + alt := d.ogTitle() 54 + if d.OGDescription != "" { 55 + alt += " — " + d.OGDescription 56 + } 57 + return alt 58 + } 59 + 47 60 // twitterCardType returns "summary_large_image" when an OG image is set, 48 61 // otherwise "summary" for the default compact card. 49 62 func (d *LayoutData) twitterCardType() string { ··· 79 92 <meta property="og:image" content={ data.OGImage }/> 80 93 <meta property="og:image:width" content="1200"/> 81 94 <meta property="og:image:height" content="630"/> 82 - <meta property="og:image:alt" content={ data.ogTitle() }/> 95 + <meta property="og:image:alt" content={ data.ogImageAlt() }/> 83 96 } 84 97 <!-- Twitter Card metadata --> 85 98 <meta name="twitter:card" content={ data.twitterCardType() }/>