OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore commits / 023ab72

Document image upload common mistakes and fix markdown rendering
Added comprehensive "Common Mistakes & Solutions" section to IMAGE_UPLOAD_LESSONS.md documenting three critical errors: 1. Backend not adding markdown reference to message text 2. Frontend showing raw markdown instead of parsing to HTML 3. Wiredove canvas not being drawn before hashing Fixes implemented: - Backend: Add `\n![](${imageHash})` to message before YAML creation - Frontend: Parse markdown `![](hash)` to `<img>` tags with parseMarkdown() - Frontend: Style inline images with .inline-image class These mistakes caused images to either not display, display with ugly markdown syntax, or have hash mismatches with the relay. Version: 4265 (2026-01-23) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
023ab72a79af392447e7f36ffd11e791d5b4a16e
by plankatron <fleeky@gmail.com> · 2026-01-23T14:35:25.000Z

Changed files (4) +274 -20

pear-app/IMAGE_UPLOAD_LESSONS.md +221 -0
……@@ -498,3 +498,224 @@ if (msg.imageData) {
498498 ```bash
499499 tcpdump -i any -A 'tcp port 9000'
500500 ```
501
502## Common Mistakes & Solutions
503
504**CRITICAL: Review this section before implementing image uploads in any client!**
505
506### Mistake 1: Backend Doesn't Add Markdown Reference
507
508**Problem (2026-01-23):**
509```javascript
510// ❌ WRONG - Backend receives imageData and creates hash, but doesn't add to message
511const yaml = YAMLHelper.create(metadata, message)
512```
513
514**Result:**
515- YAML has `image: <hash>` in metadata (profile picture field)
516- Message content is just the plain text
517- No `![](hash)` in content, so frontend can't render inline image
518- Image displays as separate attachment OR not at all
519
520**Solution:**
521```javascript
522// ✅ CORRECT - Add markdown reference to message before creating YAML
523let finalMessage = message
524if (imageHash) {
525 finalMessage = message + `\n![](${imageHash})`
526}
527const yaml = YAMLHelper.create(metadata, finalMessage)
528```
529
530**Why:** The YAML `image:` field is for profile pictures (stored in db.get('image')). Inline images must be referenced in the message content using markdown syntax.
531
532**Files affected:**
533- `pear-app/index.js` line 1177-1183 (POST /api/post handler)
534
535---
536
537### Mistake 2: Frontend Shows Raw Markdown Instead of Rendering
538
539**Problem (2026-01-23):**
540```javascript
541// ❌ WRONG - Escape HTML and show markdown literally
542<div class="message-text">${escapeHtml(msg.text)}</div>
543
544// Plus separate image rendering
545if (msg.imageData) {
546 imageHtml = `<img src="${dataUrl}" />`
547}
548```
549
550**Result:**
551```
552User message text here
553![](HBFO/tn2eTknhqkUkL9TWTiLqRwhh7EOjlCHDicT9Co=) ← Shows literally!
554[IMAGE DISPLAYS HERE] ← Shows separately!
555```
556
557**Solution:**
558```javascript
559// ✅ CORRECT - Parse markdown syntax before rendering
560function parseMarkdown(text, imageData) {
561 let html = escapeHtml(text)
562
563 if (imageData) {
564 const dataUrl = imageData.startsWith('data:')
565 ? imageData
566 : `data:image/jpeg;base64,${imageData}`
567
568 // Replace ![alt](hash) with <img> tags
569 html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, hash) => {
570 return `<img src="${dataUrl}" alt="${alt || 'Image'}" class="inline-image" />`
571 })
572 }
573
574 html = html.replace(/\n/g, '<br>')
575 return html
576}
577
578// Use in timeline
579<div class="message-text">${parseMarkdown(msg.text, msg.imageData)}</div>
580```
581
582**Result:**
583```
584User message text here
585[IMAGE DISPLAYS INLINE] ← Clean rendering!
586```
587
588**Why:** Markdown is a formatting syntax that must be parsed into HTML. Showing `![](hash)` literally is confusing and ugly.
589
590**Files affected:**
591- `pear-app/public/app-simple.js` lines 1044-1072 (parseMarkdown function)
592- `pear-app/public/app-simple.js` lines 100-115 (renderTimeline)
593- `pear-app/public/style.css` lines 247-258 (inline-image styling)
594
595---
596
597### Mistake 3: Wiredove Canvas Not Drawn To
598
599**Problem (2026-01-23):**
600```javascript
601// ❌ WRONG - Create canvas but never draw to it
602const canvas = document.createElement("canvas")
603const ctx = canvas.getContext("2d")
604const img = new Image()
605
606img.onload = async () => {
607 const croppedImage = canvas.toDataURL() // Empty canvas!
608 const hash = await apds.make(img.src) // Hash original
609 // ...
610}
611```
612
613**Result:**
614- `croppedImage` is an empty canvas (never drawn)
615- `img.src` is hashed directly (might work, but inconsistent)
616- Resize/compression code commented out but canvas still created (dead code)
617
618**Solution:**
619```javascript
620// ✅ CORRECT - Draw to canvas before converting to data URL
621img.onload = async () => {
622 // Set canvas to image size
623 canvas.width = img.width
624 canvas.height = img.height
625
626 // Draw image to canvas
627 ctx.drawImage(img, 0, 0)
628
629 // Get data URL from canvas (JPEG, 90% quality)
630 const dataUrl = canvas.toDataURL('image/jpeg', 0.9)
631
632 // Hash the canvas output
633 const hash = await apds.make(dataUrl)
634 const mdimg = `![](${hash})`
635}
636```
637
638**Why:**
639- Consistency: All systems should hash the same data URL format
640- Compression: `canvas.toDataURL('image/jpeg', 0.9)` compresses images
641- Future-proof: Easy to add resizing by changing canvas dimensions
642
643**Files affected:**
644- `evbogue/wiredove/upload.js` lines 21-55
645
646---
647
648### Checklist for Image Upload Implementation
649
650When implementing image uploads in ANY wirecore/wiredove client:
651
652**Backend (index.js, wiredove-cli):**
653- [ ] Receive image as base64 from frontend
654- [ ] Reconstruct data URL: `data:image/jpeg;base64,${base64}`
655- [ ] Hash the data URL (not raw bytes): `ANProto.hash(dataUrl)`
656- [ ] **ADD markdown to message:** `message + '\n![](${imageHash})'`
657- [ ] Store data URL in feed entry: `entry.imageData = dataUrl`
658- [ ] Create YAML with updated message (not original)
659- [ ] Send to relay as separate WebSocket message
660
661**Frontend (app-simple.js, wiredove/upload.js):**
662- [ ] Upload via file input: `<input type="file" accept="image/*">`
663- [ ] Read as data URL: `FileReader.readAsDataURL(file)`
664- [ ] Draw to canvas: `canvas.toDataURL('image/jpeg', 0.9)`
665- [ ] Hash data URL: `apds.make(dataUrl)` or `ANProto.hash(dataUrl)`
666- [ ] Send base64 part to backend (strip `data:image/jpeg;base64,` prefix)
667- [ ] **Parse markdown when displaying:** Convert `![](hash)` to `<img>`
668- [ ] Style inline images: `.inline-image { max-width: 100%; }`
669
670**Testing:**
671- [ ] Post with image from web UI
672- [ ] Verify markdown in YAML: Should contain `![](hash)`
673- [ ] Check pub.wiredove.net: Image should display
674- [ ] Check wirecore timeline: Image should display inline (not with markdown shown)
675- [ ] Verify hash: `curl -I https://pub.wiredove.net/<hash>` → HTTP 200
676
677---
678
679### Version History with Mistake Fixes
680
681- **v4257** (2026-01-22): Phase 3 complete - Original working version
682 - Handle both old (base64) and new (data URL) formats
683
684- **v4261** (2026-01-23): Fixed backend markdown reference
685 - Backend now adds `\n![](${imageHash})` to message
686 - YAML contains full content with inline image reference
687 - **BUG:** Frontend still showing raw markdown + image separately
688
689- **v4265** (2026-01-23): Fixed frontend markdown parsing ✅
690 - Added `parseMarkdown()` function to convert `![](hash)` to `<img>`
691 - Images now render inline without showing markdown syntax
692 - Clean display: text + image, no ugly `![](hash)` visible
693 - **wiredove fixed too:** Canvas drawing implemented properly
694
695---
696
697## Implementation Reference
698
699When you (Claude) are asked to work on wirecore image uploads or debug image display issues, **ALWAYS review this "Common Mistakes" section first** to avoid repeating these errors.
700
701### Quick Diagnostic Questions:
702
7031. **Image not appearing?**
704 - Check: Did backend add `![](${imageHash})` to message?
705 - Check: Was image sent as separate WebSocket message?
706 - Check: Does hash match between YAML and blob?
707
7082. **Markdown showing literally?**
709 - Check: Is frontend calling `parseMarkdown()`?
710 - Check: Is `escapeHtml()` being used without markdown parsing?
711 - Fix: Add markdown parser before rendering
712
7133. **Hash mismatch / 404 errors?**
714 - Check: Are you hashing data URLs (not raw bytes)?
715 - Check: Is canvas being drawn to before `toDataURL()`?
716 - Fix: Hash what you send (data URL format)
717
7184. **Images showing twice?**
719 - Check: Are you rendering both parsed markdown AND separate imageHtml?
720 - Fix: Only render parsed markdown (includes inline images)
721
pear-app/index.js +7 -1
……@@ -1174,13 +1174,19 @@ async function handleAPI(req, res, url) {
11741174 imageHash = ANProto.hash(dataUrl)
11751175 }
11761176
1177 // Add image markdown reference to message if image present
1178 let finalMessage = message
1179 if (imageHash) {
1180 finalMessage = message + `\n![](${imageHash})`
1181 }
1182
11771183 // Create YAML format for relay compatibility
11781184 const metadata = {
11791185 name: username || null,
11801186 image: imageHash,
11811187 previous: previousHash
11821188 }
1183 const yaml = YAMLHelper.create(metadata, message)
1189 const yaml = YAMLHelper.create(metadata, finalMessage)
11841190
11851191 // Phase 3: Hash content and store in contentCache
11861192 const contentHash = ANProto.hash(yaml)
pear-app/public/app-simple.js +31 -19
……@@ -101,23 +101,8 @@ function renderTimeline() {
101101 const time = new Date(msg.timestamp).toLocaleString()
102102 const isOwn = msg.isOwn
103103
104 // Add image if present
105 let imageHtml = ''
106 if (msg.imageData) {
107 // Check if imageData is already a data URL or just base64
108 let dataUrl
109 if (msg.imageData.startsWith('data:')) {
110 // Already a data URL (new format)
111 dataUrl = msg.imageData
112 } else {
113 // Just base64 (old format) - construct data URL
114 dataUrl = `data:${msg.imageMimeType || 'image/jpeg'};base64,${msg.imageData}`
115 }
116 imageHtml = `<div class="message-image"><img src="${dataUrl}" alt="Attached image" /></div>`
117 } else if (msg.image) {
118 // Old format: path-based (backward compatibility)
119 imageHtml = `<div class="message-image"><img src="/api/image${msg.image}" alt="Attached image" /></div>`
120 }
104 // Parse markdown in text (handles inline images via ![](hash))
105 const messageHtml = parseMarkdown(msg.text, msg.imageData)
121106
122107 return `
123108 <div class="message ${isOwn ? 'own-message' : ''}">
……@@ -125,8 +110,7 @@ function renderTimeline() {
125110 <div class="author">${escapeHtml(msg.author || 'Anonymous')}</div>
126111 <div class="timestamp">${time}</div>
127112 </div>
128 <div class="message-text">${escapeHtml(msg.text)}</div>
129 ${imageHtml}
113 <div class="message-text">${messageHtml}</div>
130114 </div>
131115 `
132116 }).join('')
……@@ -1042,6 +1026,34 @@ function escapeHtml(text) {
10421026 return div.innerHTML
10431027}
10441028
1029// Parse markdown image syntax: ![](hash) -> <img> tags
1030function parseMarkdown(text, imageData) {
1031 // First escape HTML
1032 let html = escapeHtml(text)
1033
1034 // Then parse markdown images: ![](hash) or ![alt](hash)
1035 // Replace with img tags using the stored imageData if available
1036 if (imageData) {
1037 // Determine data URL format
1038 let dataUrl
1039 if (imageData.startsWith('data:')) {
1040 dataUrl = imageData
1041 } else {
1042 dataUrl = `data:image/jpeg;base64,${imageData}`
1043 }
1044
1045 // Replace all ![...](hash) with <img> tags
1046 html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, hash) => {
1047 return `<img src="${dataUrl}" alt="${alt || 'Image'}" class="inline-image" />`
1048 })
1049 }
1050
1051 // Basic line breaks: convert \n to <br>
1052 html = html.replace(/\n/g, '<br>')
1053
1054 return html
1055}
1056
10451057// Initialize when DOM is ready
10461058if (document.readyState === 'loading') {
10471059 document.addEventListener('DOMContentLoaded', init)
pear-app/public/style.css +15 -0
……@@ -243,6 +243,21 @@ button:disabled {
243243 opacity: 0.9;
244244}
245245
246/* Inline images (from markdown) */
247.message-text .inline-image {
248 max-width: 100%;
249 height: auto;
250 border-radius: 8px;
251 margin: 0.75em 0;
252 display: block;
253 cursor: zoom-in;
254 transition: opacity 0.2s;
255}
256
257.message-text .inline-image:hover {
258 opacity: 0.9;
259}
260
246261/* Image Lightbox */
247262.image-lightbox {
248263 position: fixed;