Monorepo for Aesthetic.Computer aesthetic.computer
at main 207 lines 8.8 kB view raw view rendered
1# Keeps Permalink Modals — keep.kidlisp.com/$code 2 3**Date:** March 12, 2026 4**Status:** Plan 5**Goal:** Replace objkt.com link-outs with in-page detail modals, add shareable permalink URLs, and enable rich Twitter/X social cards for each minted keep. 6 7--- 8 9## Context 10 11The keep.kidlisp.com market tab currently shows KidLisp keeps as cards that link directly to objkt.com. This sends users away from the AC ecosystem. The Tezos NFT market landscape report (March 12, 2026) notes that "discovery and collector relationship management matter more than squeezing the floor." Permalinks with social cards directly serve discovery — when someone shares `keep.kidlisp.com/$cow` on Twitter, it should unfurl into a rich card with the piece's animated thumbnail and market status, driving traffic back to the keeps site rather than objkt. 12 13**KidLisp collection snapshot:** 34 items, 9 owners, floor 12 XTZ, total volume 254.5 XTZ, 1 active listing. 14 15--- 16 17## URL Pattern: `keep.kidlisp.com/$code` 18 19**Recommendation: Use `keep.kidlisp.com/$code`** (not `buy.kidlisp.com`). 20 21Reasons: 22- `$` prefix is already the canonical KidLisp piece naming convention 23- Short, memorable, consistent with AC's "memorizable paths" philosophy 24- No new DNS records, Netlify config, or separate site needed 25- `$` is URL-safe (no percent-encoding required) 26- Example: `keep.kidlisp.com/$cow` 27 28--- 29 30## Phase 1: Keep Detail Modal (Client-Side) 31 32**File:** `system/public/kidlisp.com/keeps.html` 33 34### 1a. Modal HTML 35Add `keep-detail-overlay` div (same pattern as existing `mintOverlay`): 36- Large animated WebP thumbnail (from IPFS `thumbnail_uri`) 37- Piece name (`$code`), token ID 38- Market status: "For Sale — 12 XTZ" or "Sold" or "Not Listed" 39- Seller/buyer/owner addresses (truncated) 40- Minted date 41- Prominent "Buy on objkt.com" button (or "View on objkt.com" if sold/unlisted) 42- "Copy Permalink" button 43- "Share on X" button → `twitter.com/intent/tweet?url=...&text=...` 44 45### 1b. Modal CSS 46Follow mint modal conventions: 47- Fixed overlay, backdrop blur, `z-index: 20000` 48- `.keep-detail-overlay.open` with fade-in animation 49- `.keep-detail-modal` max-width 520px, responsive 50- Thumbnail area at top, metadata below, action buttons at bottom 51 52### 1c. Modal JS 53- `openKeepDetailModal(entry)` — populate fields, add `.open`, push URL state, lock body scroll 54- `closeKeepDetailModal()` — remove `.open`, restore scroll, pop state to `/market` 55- Escape key + backdrop click to close (same pattern as lines 3998-4003) 56- For tokens not in current market data, fetch directly from objkt GraphQL by name 57 58### 1d. Change Market Card Click Behavior 59Currently (line 3941): 60```html 61<a class="market-card" href="${objktUrl}" target="_blank"> 62``` 63Change to: 64```html 65<div class="market-card" onclick="openKeepDetailModal(index)" role="button" tabindex="0"> 66``` 67Store sorted entries in a module-level array for index-based lookup. 68 69--- 70 71## Phase 2: URL Routing for Permalinks 72 73**File:** `system/public/kidlisp.com/keeps.html` 74 75### 2a. Extend `tabFromLocation()` (line 3961) 76Recognize `$`-prefixed paths: 77```javascript 78const seg = location.pathname.replace(/^\/+/, '').split('/')[0]; 79if (seg.startsWith('$')) return { tab: 'market', code: seg.slice(1) }; 80``` 81Update all call sites to handle the new return shape. 82 83### 2b. Deep-link on Page Load 84- Set `pendingDeepLinkCode` when URL has `$code` 85- After `loadMarket()` + `renderMarket()` complete, find matching token and auto-open modal 86- If token not in active listings/sales, fetch token metadata from objkt GraphQL by name 87- Show loading state in modal while fetching 88 89### 2c. pushState Integration 90- Open modal: `history.pushState({}, '', '/$' + code)` 91- Close modal: `history.pushState({}, '', '/market')` 92- Handle popstate for browser back/forward 93 94--- 95 96## Phase 3: Twitter/X Social Cards (Server-Side Meta Tags) 97 98Twitter/Facebook crawlers don't run JS, so OG tags must be in the initial HTML. 99 100### Approach: Netlify Edge Function 101 102**New file:** `system/netlify/edge-functions/keeps-social.js` 103 104```javascript 105export default async function(request, context) { 106 const url = new URL(request.url); 107 const host = request.headers.get('host') || ''; 108 if (!host.includes('keep.kidlisp.com')) return context.next(); 109 110 const seg = url.pathname.replace(/^\/+/, '').split('/')[0]; 111 if (!seg.startsWith('$')) return context.next(); 112 113 const ua = request.headers.get('user-agent') || ''; 114 const isCrawler = /twitterbot|facebookexternalhit|linkedinbot|slackbot|discordbot/i.test(ua); 115 if (!isCrawler) return context.next(); // SPA handles normal users 116 117 const code = seg.slice(1); 118 // Fetch token from objkt GraphQL → get name, price, thumbnail 119 // Build OG image URL: oven.aesthetic.computer/preview/1200x630/CODE.png 120 // Inject meta tags into keeps.html and return 121} 122``` 123 124### Meta Tags Injected 125```html 126<meta property="og:url" content="https://keep.kidlisp.com/$CODE" /> 127<meta property="og:title" content="$CODE · KidLisp Keep" /> 128<meta property="og:description" content="For Sale — 12 XTZ | KidLisp generative art on Tezos" /> 129<meta property="og:image" content="https://oven.aesthetic.computer/preview/1200x630/CODE.png" /> 130<meta name="twitter:card" content="summary_large_image" /> 131<meta name="twitter:title" content="$CODE · KidLisp Keep" /> 132<meta name="twitter:description" content="For Sale — 12 XTZ | KidLisp generative art on Tezos" /> 133<meta name="twitter:image" content="https://oven.aesthetic.computer/preview/1200x630/CODE.png" /> 134``` 135 136### OG Image Strategy 137- **Twitter/X cards**: Static PNG via `oven.aesthetic.computer/preview/1200x630/CODE.png` (already working infrastructure, 24h CDN cache) 138- **In-page modal**: Animated WebP via IPFS `thumbnail_uri` (shows animation in browser) 139- Twitter doesn't support animated images in cards — static PNG is the correct format 140 141### Netlify Config 142**File:** `system/netlify.toml` — add edge function binding: 143```toml 144[[edge_functions]] 145function = "keeps-social" 146path = "/*" 147``` 148(Host filtering done inside the function since edge functions may not support subdomain-scoped paths.) 149 150### Fallback 151If edge functions don't work well with subdomain routing, fall back to modifying `system/netlify/functions/index.mjs` (the keep.kidlisp.com handler around line 182) to detect `$code` paths and inject meta tags there. This is slightly slower but is a proven pattern used for `top.kidlisp.com`. 152 153--- 154 155## Phase 4: OG Image Polish (Optional) 156 157**File:** `oven/server.mjs` 158 159Add a dedicated `/keeps/og/$code.png` endpoint that generates a styled 1200x630 card: 160- Piece thumbnail (static frame from WebP) centered on branded background 161- `$code` name overlaid 162- Price/status text 163- KidLisp + keeps branding 164 165This is a nice-to-have — the existing `/preview/` endpoint works fine for MVP. 166 167--- 168 169## Implementation Order 170 171| Step | Scope | Files | Shippable? | 172|------|-------|-------|------------| 173| Phase 1 | Client-side detail modal | `keeps.html` | Yes | 174| Phase 2 | URL routing + deep-links | `keeps.html` | Yes (with Phase 1) | 175| Phase 3 | SSR meta tags for social cards | `keeps-social.js`, `netlify.toml` | Yes | 176| Phase 4 | Branded OG images | `oven/server.mjs` | Optional polish | 177 178Phases 1+2 ship as one commit. Phase 3 is a separate commit. Phase 4 is independent. 179 180--- 181 182## Critical Files 183 184| File | Changes | 185|------|---------| 186| `system/public/kidlisp.com/keeps.html` | Modal HTML/CSS/JS, card click handlers, URL routing, deep-link logic | 187| `system/netlify/edge-functions/keeps-social.js` | **New** — crawler detection + SSR meta tag injection | 188| `system/netlify.toml` | Edge function binding for keeps-social | 189| `system/netlify/functions/index.mjs` | Fallback SSR approach if edge function doesn't work for subdomains | 190 191### Reuse Existing Infrastructure 192- `fetchObjktGraphQL()` (keeps.html:3718) — already handles objkt queries with retries 193- `shortAddress()` (keeps.html:3707) — address truncation 194- `getKeepsContractAddress()` — contract address resolution 195- Mint modal open/close pattern (keeps.html:3997-4003) — exact same UX for detail modal 196- `oven.aesthetic.computer/preview/1200x630/CODE.png` — existing OG image generation 197- `oven.aesthetic.computer/keeps/latest/:piece` — per-piece thumbnail lookup 198 199--- 200 201## Verification 202 2031. **Modal**: Click a market card → modal opens with piece details, animated thumbnail, market status. Escape/backdrop closes it. 2042. **Permalink**: Navigate to `keep.kidlisp.com/$cow` → market tab activates, modal auto-opens for `$cow`. 2053. **Copy/Share**: Copy permalink button copies correct URL. Share on X opens tweet intent with URL. 2064. **Social card**: Use Twitter Card Validator or `curl -A Twitterbot keep.kidlisp.com/$cow` → verify OG tags are present with correct title, description, and image URL. 2075. **Browser back/forward**: Open modal → press back → modal closes, URL returns to `/market`.