gabriel / musehub public
Open #129 Enhancement
filed by gabriel human · 9 days ago · assigned to aaronrene

Rich social preview cards for repo links -- no dynamic OG image today

0 Anchors
Blast radius
Churn 30d
0 Proposals

Background

Pasting a MuseHub repo link into Facebook's post composer produces a bare-text preview (title + "MuseHub" subtitle, no image, no stats). Pasting a GitHub repo link produces a rich card: owner avatar, repo name, description, contributor/issue/star/fork counts, and a colored language bar — because GitHub dynamically generates an actual preview image server-side and serves it via og:image.

Root cause, traced in code, not assumed: _og_tags() in musehub/api/routes/musehub/_ui_helpers.py:38-58 already supports an image parameter and correctly emits og:image/twitter:image when one is given:

def _og_tags(
    *, title: str, description: str = "", image: str = "",
    og_type: str = "website", twitter_card: str = "summary",
) -> StrDict:
    ...
    if image:
        tags["og:image"] = image
        tags["twitter:image"] = image
    return tags

But the repo detail page's call site (musehub/api/routes/musehub/ui_repo.py:216) never passes image at all:

"og_meta": _og_tags(
    title=f"{owner}/{repo_slug} — MuseHub",
    description=repo.description or f"Music composition repository by {owner}",
    og_type="website",
),

There is currently no image-generation capability anywhere in MuseHub for social cards — not a bug in the OG-tag plumbing, a missing feature.

Goal

Repo pages (and, once proven, other shareable entities — issues, proposals, mists, releases) serve a real og:image/twitter:image — a server-rendered PNG showing at minimum: owner avatar, repo/entity name, description, and relevant stats (stars/forks/issues for repos; state/author for issues/proposals). Muse-specific flourishes worth considering once the basic card works: domain type (code/MIDI/etc.), commit count, primary language or domain-plugin icon — MuseHub's answer to GitHub's language bar.

"Done" (for repos, the pilot entity): pasting a https://musehub.ai/<owner>/<repo> link into Facebook, X, LinkedIn, or Slack renders a card with a real image, verified against at least one real platform's link-preview debugger (Facebook Sharing Debugger, X Card Validator, or equivalent) — not just correct HTML, since several platforms cache aggressively and some (X in particular) have been known to silently ignore correct OG tags for undocumented reasons.

Out of Scope

  • Extending rich cards to every entity type in the first pass — ship repos first, prove the pattern, then decide whether issues/proposals/mists/ releases get the same treatment as a follow-up.
  • Redesigning _og_tags()'s existing shape — it's already correct; this is about supplying it an image, not changing its contract.
  • Any change to jsonld_repo()/structured data — separate concern from Open Graph, not touched here.

Phases

Phase 1 — Design: what the card looks like and how it's rendered

  • Decide the rendering approach: server-side HTML→image (e.g. a headless browser screenshot of a dedicated template) vs. a drawing library (Pillow/cairosvg) composing a fixed layout. Headless-browser rendering is closer to what Vercel's @vercel/og and GitHub's own pipeline do, and makes future layout changes a CSS edit, not a redraw-from-scratch; a drawing library has fewer runtime dependencies but a much higher cost to iterate on layout. Record the decision and why before Phase 2 starts.
  • Design the actual layout/content for a repo card (mock or written spec): what fields, what fallback when a repo has no description/no avatar, what happens for a very long repo name or description.
  • Decide the caching strategy — these images should not be regenerated on every request; decide whether to cache by repo content-hash (regenerate when name/description/stats meaningfully change) or on a TTL, and where the cache lives (object store? a dedicated table? CDN-fronted static path?).

Deliverable: a written design (this issue's body, updated) covering rendering approach, layout, and caching, reviewed before any code lands.

Phase 2 — Implement: /​{owner}/{repo}/opengraph-image endpoint

  • OG_01 — New route serving a PNG for a given repo, using Phase 1's chosen rendering approach. Correct Content-Type: image/png.
  • OG_02 — Image includes the repo name and description (or the fallback text when description is empty) — verified by rendering and inspecting actual pixel/text content, not just that the endpoint returns 200.
  • OG_03 — Image includes owner avatar when one exists, and a sane default placeholder when it doesn't.
  • OG_04 — Image includes stats (stars/forks/issues or whatever Phase 1 decided) sourced from real repo data, not hardcoded.
  • OG_05 — Caching behavior from Phase 1's design is implemented and tested: a second request for the same repo state doesn't regenerate; a request after a meaningful change does.

Deliverable: OG_01-05 green; endpoint deployed to staging.

Phase 3 — Wire into repo pages, verify on real platforms

  • ui_repo.py's _og_tags() call passes the new endpoint's URL as image.
  • OG_06og:image/twitter:image present in the rendered repo page's <head>, pointing at an absolute URL (relative URLs are unreliable across crawlers).
  • Manually verify against at least one real platform's link-preview debugger (Facebook Sharing Debugger / X Card Validator) — record the result (screenshot or debugger output) in this issue. This step cannot be meaningfully unit-tested; it's the actual acceptance gate.

Deliverable: a real, verified rich preview when a staging repo URL is pasted into at least one social platform.


Acceptance Criteria

  • A repo page serves a real, non-empty og:image.
  • The image contains repo name, description (or fallback), owner avatar (or placeholder), and real stats — not hardcoded/placeholder data.
  • Verified against at least one real platform's official preview debugger, not just HTML inspection.
  • Caching strategy from Phase 1 is implemented and tested — no regenerate-on-every-request behavior.

Implementation Order

Phase 1 → Phase 2 → Phase 3. Do not start Phase 2 until Phase 1's rendering approach and caching strategy are actually decided and written down — this is exactly the kind of feature where picking the wrong rendering approach costs a full rewrite partway through.

Activity3
gabriel opened this issue 9 days ago
gabriel 7 days ago

Assigned to @aaronrene — tracked under master queue #138.

gabriel 7 days ago

Screenshots referenced in the background above — pasting a repo link into Facebook's post composer:

GitHub link (cgcardona/musehub-backup) — rich card: owner avatar, repo name, description, contributors/issues/stars/forks stats, colored language bar.

Facebook preview of a GitHub repo link

MuseHub link (gabriel/muse) — bare text only: no image, no stats, no avatar.

Facebook preview of a MuseHub repo link

This is the concrete before/after target for Phase 3's acceptance gate.

gabriel 7 days ago

FYI aaronrene — separately fixing a layout regression (musehub#139, viewport-locked flex shell for the two-column pages) in parallel. Scope is base.html + SCSS + page-template layout markup only — shouldn't touch anything in _ui_helpers.py, ui_repo.py's OG wiring, or the new opengraph-image endpoint you're building here. Flagging so we don't collide if we're both touching ui_repo.py's template around the same time.