Blogging platform with advanced tools for arts and sciences.
6

Configure Feed

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

Add feed button to publications

lemma (Jun 7, 2026, 4:07 PM -0500) 6664f0be 230bf1af

+44 -19
+44 -19
app/routes/profile.tsx
··· 12 12 import CenteredMessage from '~/components/CenteredMessage' 13 13 import PageLayout from '~/components/PageLayout' 14 14 import Spinner from '~/components/Spinner' 15 + import { RssIcon } from '@heroicons/react/24/outline' 15 16 import type { Route } from './+types/profile' 16 17 import { SITE_NAME } from '~/lib/site-config' 17 18 ··· 30 31 // publicationAtUri → subscriptionRkey; undefined = not yet loaded 31 32 const [subscriptionsByPub, setSubscriptionsByPub] = useState<Record<string, string> | undefined>(undefined) 32 33 const [loadingPubs, setLoadingPubs] = useState<Record<string, boolean>>({}) 34 + const [copiedPubUri, setCopiedPubUri] = useState<string | null>(null) 33 35 34 36 useEffect(() => { 35 37 if (!id) return ··· 127 129 } 128 130 } 129 131 132 + async function handleCopyFeed(pub: Publication) { 133 + const feedUrl = `${pub.url.replace(/\/$/, '')}/feed.xml` 134 + try { 135 + await navigator.clipboard.writeText(feedUrl) 136 + setCopiedPubUri(pub.atUri) 137 + setTimeout(() => setCopiedPubUri((cur) => (cur === pub.atUri ? null : cur)), 2500) 138 + } catch { /* ignore */ } 139 + } 140 + 130 141 if (error) { 131 142 return <CenteredMessage message={error} error action={{ label: 'Go home', to: '/' }} /> 132 143 } ··· 166 177 {pub.description && ( 167 178 <p className="text-sm text-stone-500 dark:text-stone-400 truncate mt-0.5">{pub.description}</p> 168 179 )} 180 + {copiedPubUri === pub.atUri && ( 181 + <p className="text-xs text-green-600 dark:text-green-400 mt-1"> 182 + Feed URL copied — paste into your RSS reader 183 + </p> 184 + )} 169 185 </div> 170 - {showSubscribeButtons && ( 171 - isSubscribed ? ( 172 - <button 173 - onClick={() => handleUnsubscribe(pub)} 174 - disabled={isLoading} 175 - className="shrink-0 text-sm px-3 py-1 rounded-full border border-stone-300 dark:border-stone-600 text-stone-600 dark:text-stone-300 hover:border-stone-400 dark:hover:border-stone-500 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors disabled:opacity-50" 176 - > 177 - {isLoading ? 'Unsubscribing…' : 'Subscribed'} 178 - </button> 179 - ) : ( 180 - <button 181 - onClick={() => handleSubscribe(pub)} 182 - disabled={isLoading} 183 - className="shrink-0 text-sm px-3 py-1 rounded-full bg-stone-900 dark:bg-stone-100 text-stone-50 dark:text-stone-900 hover:bg-stone-700 dark:hover:bg-stone-200 transition-colors disabled:opacity-50" 184 - > 185 - {isLoading ? 'Subscribing…' : 'Subscribe'} 186 - </button> 187 - ) 188 - )} 186 + <div className="shrink-0 flex items-center gap-2"> 187 + <button 188 + onClick={() => handleCopyFeed(pub)} 189 + title="Copy Atom feed URL" 190 + className="p-1.5 rounded-md text-stone-400 dark:text-stone-500 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-stone-100 dark:hover:bg-stone-800 transition-colors" 191 + > 192 + <RssIcon className="h-4 w-4" /> 193 + </button> 194 + {showSubscribeButtons && ( 195 + isSubscribed ? ( 196 + <button 197 + onClick={() => handleUnsubscribe(pub)} 198 + disabled={isLoading} 199 + className="text-sm px-3 py-1 rounded-full border border-stone-300 dark:border-stone-600 text-stone-600 dark:text-stone-300 hover:border-stone-400 dark:hover:border-stone-500 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors disabled:opacity-50" 200 + > 201 + {isLoading ? 'Unsubscribing…' : 'Subscribed'} 202 + </button> 203 + ) : ( 204 + <button 205 + onClick={() => handleSubscribe(pub)} 206 + disabled={isLoading} 207 + className="text-sm px-3 py-1 rounded-full bg-stone-900 dark:bg-stone-100 text-stone-50 dark:text-stone-900 hover:bg-stone-700 dark:hover:bg-stone-200 transition-colors disabled:opacity-50" 208 + > 209 + {isLoading ? 'Subscribing…' : 'Subscribe'} 210 + </button> 211 + ) 212 + )} 213 + </div> 189 214 </li> 190 215 ) 191 216 })}