Skip to content

Session share card — the detail hero is the object you share ​

Status: Active · Date: 2026-09-20 · Owner: "I want the hero to become a sharable session card. Currently it's just a collection of badges even though our session summary card has largely moved away from badges. And components and primitives inside the session card are a mix of typography components, primitives and design patterns that are not synchronized and in line with our design principles." · Board: /wireframes/session-share-card.

Research inputs: detail-page inventory (hero = TypeHero/MediaHero with a chip-density GlyphFactRow; the nav subtitle, 정보's 시작 시간 and the venue card's map restate the hero; sharing is a text string + link, the Kakao rail a console-reviewed CUSTOM template with no image, the OG image a generic green card; no share surface exists in SESSION_SURFACE_PROJECTION; react-native-view-shot/expo-sharing are NOT installed); benchmark (Luma/Meetup generate the share image from the same asset as the in-app hero; Kakao FEED template accepts a per-call image URL, ≤2-line description, ≤8-char button; 1200×630 is the universal unfurl size; Wallet passes put place · when · status on one face; on-device screenshot shares bake viewer state into what strangers receive).

Decisions ​

#DecisionWhy
S1One projection, one card. SessionSurface gains 'share'; buildSessionShareModel(model, { clubName }) (pure, packages/app/src/domain/session-info-model/share-model.ts) yields the viewer-neutral fields: clubName, venueName, recruitment (derived status word/tone — never the viewer's standing), dateLabel, timeLabel, formatLabel (sessionTitle), tierLabel (formatSkillRange, never 🔒), confirmedCount, maxPlayers, mapKey (static-map cache key). share-cards.ts's text/Kakao payloads read it instead of re-deriving.Canon: every surface is a projection; the share text was the one ad-hoc deriver.
S2SessionShareCard (packages/app/src/presentation/components/sessions/session-share-card.tsx) is the detail hero: 2:1 media band (RemoteImage of the caller's mapUri, NAVER attribution as the venue tile draws it, Pressable affordance="layer" → 테니스장) → identity block on SEAM.hero: META_TEXT.eyebrow club name, pageTitle venue (one line) + RecruitmentStatusBadge, two GlyphFactRow density="inline" rows (📅 date · ⏰ time / 🎾 format+type · 🎯 tier, SESSION_FACT_GLYPH), RecruitmentMeter showStatusWord={false}. No fee, no faces, no viewer state.The list card's grammar opened up; density inline (the owner named the chips). Viewer-neutral by construction = forwardable.
S3Share = link; the link unfurls to the card. Web /s/[id] renders the card (the OG image inline + open-in-app); apps/web/lib/og-card.tsx becomes the 1200×630 card in the list card's tile+column layout (630px square map tile left — the public static-map-cache storage PNG by cache key, glyph tile when absent — column right: eyebrow, venue, 📅·⏰, 🎾·🎯, status + N/M). get_public_share_preview('session') grows the fields the card needs (venue name, tier band, event type, is_friendly, max players, confirmed count, recruitment inputs, lat/lng). Native rail: shareFeedTemplate (image = the OG route URL, title = venue, description = date time / tier, one 참여하기 button → the share URL) with the OS share sheet as fallback; the custom template stays only for non-session shares.Server-rendered image: every unfurl (Kakao, iMessage, Slack) matches; no native dep; no viewer state baked in.
S4The page stops restating the card. DetailShell title bare (no format/date subtitle); StatStrip = 내 상태 · 내 참가비 · 신청 마감 (확정 인원 leaves — the meter has it); 정보 = 경기 유형 · 경기 방식 · 최대 인원 · 대진 방식 (시작 시간 and 참가비 leave); the 테니스장 block drops its map (rows: 주소 · 코트 · 운영 + 상세 link — the band is the map); 공지's 📣 renders through GlyphSlot.Each fact once; viewer facts below the card.
S5Rejected: on-device screenshot share (needs react-native-view-shot + expo-sharing — native, no OTA; bakes 🔒/standing into a forwarded image); chip density on the hero; Kakao custom template for sessions; faces on the shared image (PII).

Slices (parallel, disjoint files) ​

  • A — app core (packages/app, packages/test-kit): S1 model + projection entry, SessionShareCard, share-cards.ts on the model, share.native.ts feed template, exports, tests.
  • B — detail page (packages/features/sessions): S2 wiring (hero section → card, map via useVenueHeroImage), S4 dedupe, notice GlyphSlot, tests, blueprint, device list.
  • C — web + data (apps/web, supabase): S3 OG card + share page + RPC migration (pgTAP), pushed to the hosted bed.

Verification: yarn check; web render of /sessions/[id] and /s/[id]; OG route rendered to PNG and read; preview OTA; emulator screenshot of the detail page.

Markdown remains the source of truth. Run yarn docs:check before handoff.