🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

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

Optimize load perfs

juprodh d3c61ac1 2482b104

+59 -15
+12 -6
deploy/Caddyfile
··· 5 5 6 6 encode gzip 7 7 8 - # Immutable blob cache headers (PDS-proxied blobs) 8 + # Immutable blob cache (only if blobs are content-addressed / versioned) 9 9 handle /blob/* { 10 10 header Cache-Control "public, max-age=31536000, immutable" 11 11 reverse_proxy localhost:3000 12 12 } 13 13 14 - # Static assets with long cache 14 + # Static assets served from disk 15 15 handle /public/* { 16 - header Cache-Control "public, max-age=86400" 16 + uri strip_prefix /public 17 + root * /var/www/lichen/public 18 + file_server 19 + 20 + header Cache-Control "public, max-age=31536000, immutable" 21 + } 22 + 23 + # HTML microcache 24 + handle { 25 + header Cache-Control "public, max-age=2" 17 26 reverse_proxy localhost:3000 18 27 } 19 - 20 - # Everything else 21 - reverse_proxy localhost:3000 22 28 } 23 29 24 30 stats.lichen.wiki {
+14 -2
src/views/layout.ts
··· 173 173 <head> 174 174 <meta charset="UTF-8"> 175 175 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 176 + <meta name="description" content="Lichen lets you build shared wikis with friends on AT Protocol. Create knowledge bases for hobbies, activism, and more—your data stays on your PDS."> 176 177 <title>${escapeHtml( 177 178 options?.wikiName 178 179 ? options.currentNoteSlug ··· 192 193 } 193 194 <link rel="stylesheet" href="/public/dist.css"> 194 195 ${extraStyles} 195 - <script src="https://unpkg.com/htmx.org@2.0.4"></script> 196 + <script src="/public/htmx.min.js" defer></script> 196 197 ${extraScripts} 197 198 </head> 198 199 <body class="${THEME.bgPage} ${THEME.textPrimary} min-h-screen flex flex-col"> 199 200 <nav class="shrink-0 sticky top-0 z-20 ${THEME.bgSurface} border-b ${THEME.borderDefault}"> 200 201 <div class="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between gap-4"> 201 202 <div class="flex items-center gap-2 min-w-0"> 202 - <a href="/" class="flex items-center gap-2 shrink-0"><img src="/public/logo.svg" alt="" width="22" height="22" class="shrink-0"/><span class="text-lg font-semibold ${THEME.accentText}">Lichen</span></a> 203 + <a href="/" class="flex items-center gap-2 shrink-0"> 204 + <img 205 + src="/public/logo.svg" 206 + width="22" 207 + height="22" 208 + fetchpriority="high" 209 + decoding="async" 210 + alt="" 211 + class="shrink-0" 212 + /> 213 + <span class="text-lg font-semibold ${THEME.accentText}">Lichen</span> 214 + </a> 203 215 ${options?.wikiName ? `<span class="${THEME.textMuted}">/</span><a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} ${THEME.accentHoverText} truncate">${escapeHtml(options.wikiName)}</a>` : ""} 204 216 </div> 205 217 <div class="flex items-center gap-2 shrink-0">
+12 -3
src/views/profile.ts
··· 22 22 const handle = profile.handle ? `@${escapeHtml(profile.handle)}` : null; 23 23 24 24 const avatarHtml = profile.avatar 25 - ? `<img src="${escapeHtml(profile.avatar)}" alt="" class="w-16 h-16 rounded-full">` 25 + ? `<img 26 + src="${escapeHtml(profile.avatar)}" 27 + width="64" 28 + height="64" 29 + loading="lazy" 30 + decoding="async" 31 + alt="" 32 + class="w-16 h-16 rounded-full" 33 + >` 26 34 : `<div class="w-16 h-16 rounded-full ${THEME.bgPlaceholder} flex items-center justify-center ${THEME.textMuted}"> 27 - <svg class="w-8 h-8" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 28 - </div>`; 35 + <svg class="w-8 h-8" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 36 + </div>`; 29 37 30 38 const nameHtml = displayName 31 39 ? `<h1 class="text-2xl font-bold ${THEME.textPrimary}">${displayName}</h1>` 32 40 : ""; 41 + 33 42 const handleHtml = handle 34 43 ? `<p class="text-sm ${THEME.textMuted}">${handle}</p>` 35 44 : "";
+12 -3
src/views/settings.ts
··· 22 22 23 23 function renderIdentity(did: string, profile: ProfileInfo | undefined): string { 24 24 const avatar = profile?.avatar 25 - ? `<img src="${escapeHtml(profile.avatar)}" alt="" class="w-8 h-8 rounded-full inline-block mr-2 align-middle" />` 26 - : `<span class="w-8 h-8 rounded-full inline-block mr-2 align-middle ${THEME.bgPlaceholder}"></span>`; 27 - 25 + ? `<img 26 + src="${escapeHtml(profile.avatar)}" 27 + width="32" 28 + height="32" 29 + loading="lazy" 30 + decoding="async" 31 + alt="" 32 + class="w-8 h-8 rounded-full inline-block mr-2 align-middle" 33 + >` 34 + : `<span 35 + class="w-8 h-8 rounded-full inline-block mr-2 align-middle ${THEME.bgPlaceholder}" 36 + ></span>`; 28 37 const name = profile?.displayName 29 38 ? `<span class="font-medium">${escapeHtml(profile.displayName)}</span>` 30 39 : "";
+9 -1
src/views/wiki-card.ts
··· 18 18 ? `<span class="truncate">${escapeHtml(wiki.owner_handle)}</span>` 19 19 : ""; 20 20 const ownerAvatar = wiki.owner_avatar 21 - ? `<img src="${escapeHtml(wiki.owner_avatar)}" alt="${wiki.owner_handle ? `@${escapeHtml(wiki.owner_handle)}` : ""}" class="w-4 h-4 rounded-full shrink-0">` 21 + ? `<img 22 + src="${escapeHtml(wiki.owner_avatar)}" 23 + width="16" 24 + height="16" 25 + loading="lazy" 26 + decoding="async" 27 + alt="${wiki.owner_handle ? `@${escapeHtml(wiki.owner_handle)}` : ""}" 28 + class="w-4 h-4 rounded-full shrink-0" 29 + >` 22 30 : ""; 23 31 const ownerHtml = 24 32 ownerHandle || ownerAvatar