Monorepo for Aesthetic.Computer
aesthetic.computer
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`.