See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Render image/video/external embeds on profile page

Pass embed through controller mapping and add conditional Edge template
blocks for images (1-4 grid layouts), video (thumbnail with play overlay
linking to bsky.app), and external links (card with optional thumbnail).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+121
+1
app/controllers/profile_controller.ts
··· 270 270 // Add bsky.app URL to each post 271 271 const postsWithUrl = posts.map((p) => ({ 272 272 ...p, 273 + embed: p.embed, 273 274 bskyUrl: atUriToBskyUrl(p.postUri), 274 275 postTextSafe: escapeHtml(p.postText).replace(/\n/g, '<br>'), 275 276 }))
+120
resources/views/pages/profile/show.edge
··· 90 90 <div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;"> 91 91 <div style="flex: 1;"> 92 92 <p style="margin: 0 0 12px; white-space: pre-wrap; word-break: break-word;">{{{ post.postTextSafe }}}</p> 93 + 94 + {{-- Embed block --}} 95 + @if(post.embed) 96 + @if(post.embed.type === 'images') 97 + @if(post.embed.items.length === 1) 98 + <div style="margin-bottom: 12px;"> 99 + <a href="{{ post.embed.items[0].fullsize }}" target="_blank" rel="noopener" style="display: block;"> 100 + <img 101 + src="{{ post.embed.items[0].thumb }}" 102 + alt="{{ post.embed.items[0].alt }}" 103 + loading="lazy" 104 + style="max-width: 100%; max-height: 400px; border-radius: 8px; object-fit: cover;{{ post.embed.items[0].aspectRatio ? ' aspect-ratio: ' + post.embed.items[0].aspectRatio.width + ' / ' + post.embed.items[0].aspectRatio.height + ';' : '' }}" 105 + > 106 + </a> 107 + </div> 108 + @elseif(post.embed.items.length === 2) 109 + <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-bottom: 12px;"> 110 + @each(img in post.embed.items) 111 + <a href="{{ img.fullsize }}" target="_blank" rel="noopener" style="display: block;"> 112 + <img 113 + src="{{ img.thumb }}" 114 + alt="{{ img.alt }}" 115 + loading="lazy" 116 + style="width: 100%; height: 200px; border-radius: 6px; object-fit: cover;" 117 + > 118 + </a> 119 + @endeach 120 + </div> 121 + @elseif(post.embed.items.length === 3) 122 + <div style="display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 4px; margin-bottom: 12px;"> 123 + <a href="{{ post.embed.items[0].fullsize }}" target="_blank" rel="noopener" style="display: block; grid-row: 1 / 3;"> 124 + <img 125 + src="{{ post.embed.items[0].thumb }}" 126 + alt="{{ post.embed.items[0].alt }}" 127 + loading="lazy" 128 + style="width: 100%; height: 100%; border-radius: 6px; object-fit: cover;" 129 + > 130 + </a> 131 + <a href="{{ post.embed.items[1].fullsize }}" target="_blank" rel="noopener" style="display: block;"> 132 + <img 133 + src="{{ post.embed.items[1].thumb }}" 134 + alt="{{ post.embed.items[1].alt }}" 135 + loading="lazy" 136 + style="width: 100%; height: 100%; border-radius: 6px; object-fit: cover;" 137 + > 138 + </a> 139 + <a href="{{ post.embed.items[2].fullsize }}" target="_blank" rel="noopener" style="display: block;"> 140 + <img 141 + src="{{ post.embed.items[2].thumb }}" 142 + alt="{{ post.embed.items[2].alt }}" 143 + loading="lazy" 144 + style="width: 100%; height: 100%; border-radius: 6px; object-fit: cover;" 145 + > 146 + </a> 147 + </div> 148 + @elseif(post.embed.items.length >= 4) 149 + <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-bottom: 12px;"> 150 + @each(img in post.embed.items.slice(0, 4)) 151 + <a href="{{ img.fullsize }}" target="_blank" rel="noopener" style="display: block;"> 152 + <img 153 + src="{{ img.thumb }}" 154 + alt="{{ img.alt }}" 155 + loading="lazy" 156 + style="width: 100%; height: 160px; border-radius: 6px; object-fit: cover;" 157 + > 158 + </a> 159 + @endeach 160 + </div> 161 + @endif 162 + @elseif(post.embed.type === 'video') 163 + <div style="margin-bottom: 12px;"> 164 + <a href="{{ post.bskyUrl }}" target="_blank" rel="noopener" style="display: block; position: relative; max-width: 100%;"> 165 + <img 166 + src="{{ post.embed.thumbnail }}" 167 + alt="{{ post.embed.alt }}" 168 + loading="lazy" 169 + style="max-width: 100%; max-height: 400px; border-radius: 8px; object-fit: cover;{{ post.embed.aspectRatio ? ' aspect-ratio: ' + post.embed.aspectRatio.width + ' / ' + post.embed.aspectRatio.height + ';' : '' }}" 170 + > 171 + <span style=" 172 + position: absolute; 173 + top: 50%; left: 50%; 174 + transform: translate(-50%, -50%); 175 + background: rgba(0, 0, 0, 0.6); 176 + color: #fff; 177 + width: 48px; height: 48px; 178 + border-radius: 50%; 179 + display: flex; align-items: center; justify-content: center; 180 + font-size: 20px; 181 + ">&#9654;</span> 182 + </a> 183 + </div> 184 + @elseif(post.embed.type === 'external') 185 + <div style="margin-bottom: 12px;"> 186 + <a href="{{ post.embed.uri }}" target="_blank" rel="noopener" style=" 187 + display: flex; 188 + border: 1px solid #e5e5e5; 189 + border-radius: 8px; 190 + overflow: hidden; 191 + text-decoration: none; 192 + color: inherit; 193 + "> 194 + @if(post.embed.thumb) 195 + <div style="flex-shrink: 0; width: 120px; min-height: 80px;"> 196 + <img 197 + src="{{ post.embed.thumb }}" 198 + alt="" 199 + loading="lazy" 200 + style="width: 100%; height: 100%; object-fit: cover;" 201 + > 202 + </div> 203 + @endif 204 + <div style="padding: 10px 12px; min-width: 0; flex: 1;"> 205 + <div style="font-weight: 600; font-size: 0.9rem; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ post.embed.title }}</div> 206 + <div style="font-size: 0.8rem; color: #888; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">{{ post.embed.description }}</div> 207 + </div> 208 + </a> 209 + </div> 210 + @endif 211 + @endif 212 + 93 213 <div style="font-size: 0.85rem; color: #888; display: flex; gap: 16px; flex-wrap: wrap;"> 94 214 <span>♥ {{ post.likes }} likes</span> 95 215 <span>♻ {{ post.reposts }} reposts</span>