A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

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

Add terms of service page and update login/error UI styling

+133 -50
+1
internal/server/server.go
··· 218 218 s.router.Get("/xrpc/at.glean.getRecommendations", xrpc.GetRecommendations) 219 219 s.router.Get("/xrpc/at.glean.listFeedLists", xrpc.ListFeedLists) 220 220 221 + s.router.Get("/terms", s.handleTerms) 221 222 s.router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(static.Files)))) 222 223 s.router.Handle("/metrics", promhttp.Handler()) 223 224 s.router.Get("/stats", s.handleStats)
+7
internal/server/terms_handler.go
··· 1 + package server 2 + 3 + import "net/http" 4 + 5 + func (s *Server) handleTerms(w http.ResponseWriter, r *http.Request) { 6 + s.render(w, r, "terms.html", map[string]any{}) 7 + }
+7 -7
internal/tmpl/404.html
··· 1 1 {{define "404.html"}} 2 - <div class="max-w-md mx-auto mt-20"> 3 - <div class="bg-spot-surface rounded-xl shadow-spot-heavy p-8"> 4 - <div class="flex justify-center mb-6"> 5 - <span class="w-14 h-14">{{template "logo-icon"}}</span> 2 + <div class="flex items-center justify-center min-h-screen px-4 py-12"> 3 + <div class="max-w-sm w-full"> 4 + <div class="flex justify-center mb-8"> 5 + <a href="/" class="w-16 h-16 block">{{template "logo-icon"}}</a> 6 6 </div> 7 - <h1 class="text-2xl font-bold text-spot-text mb-2 text-center">Page not found</h1> 8 - <p class="text-spot-secondary text-sm mb-6 text-center">This page doesn't exist, but your feeds do.</p> 9 - <a href="/dashboard" class="w-full flex items-center justify-center gap-2 bg-spot-green text-white rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 7 + <h1 class="text-2xl font-bold text-spot-text mb-1 text-center">Page not found</h1> 8 + <p class="text-spot-secondary text-sm mb-8 text-center">This page doesn't exist, but your feeds do.</p> 9 + <a href="/dashboard" class="w-full flex items-center justify-center gap-2 bg-spot-green text-white rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 10 10 Back to Dashboard 11 11 </a> 12 12 </div>
+5 -5
internal/tmpl/base.html
··· 210 210 </button> 211 211 </div> 212 212 </div> 213 - <div class="text-right"> 214 - <div class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} <a href="https://bsky.app/profile/julien.rbrt.fr" class="hover:text-spot-text transition">julien.rbrt.fr</a></div> 215 - <div class="text-[10px] text-spot-secondary mt-0.5">Made in Europe &#127466;&#127482;</div> 216 - </div> 213 + <div class="text-right"> 214 + <div class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} <a href="https://bsky.app/profile/julien.rbrt.fr" class="hover:text-spot-text transition">julien.rbrt.fr</a></div> 215 + <div class="text-[10px] text-spot-secondary mt-0.5">Made in Europe &#127466;&#127482; &middot; <a href="/terms" class="hover:text-spot-text transition">Terms</a></div> 216 + </div> 217 217 </div> 218 218 219 219 <div class="md:hidden flex flex-col gap-5"> ··· 246 246 </button> 247 247 </div> 248 248 <div class="border-t border-spot-divider mt-4 pt-3"> 249 - <span class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} <a href="https://bsky.app/profile/julien.rbrt.fr" class="hover:text-spot-text transition">julien.rbrt.fr</a> &middot; Made in Europe &#127466;&#127482;</span> 249 + <span class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} <a href="https://bsky.app/profile/julien.rbrt.fr" class="hover:text-spot-text transition">julien.rbrt.fr</a> &middot; Made in Europe &#127466;&#127482; &middot; <a href="/terms" class="hover:text-spot-text transition">Terms</a></span> 250 250 </div> 251 251 </div> 252 252 </div>
+7 -7
internal/tmpl/error.html
··· 1 1 {{define "error.html"}} 2 - <div class="max-w-md mx-auto mt-20"> 3 - <div class="bg-spot-surface rounded-xl shadow-spot-heavy p-8"> 4 - <div class="flex justify-center mb-6"> 5 - <span class="w-14 h-14">{{template "logo-icon"}}</span> 2 + <div class="flex items-center justify-center min-h-screen px-4 py-12"> 3 + <div class="max-w-sm w-full"> 4 + <div class="flex justify-center mb-8"> 5 + <a href="/" class="w-16 h-16 block">{{template "logo-icon"}}</a> 6 6 </div> 7 - <h1 class="text-2xl font-bold text-spot-text mb-2 text-center">{{.Title}}</h1> 8 - <p class="text-spot-secondary text-sm mb-6 text-center">{{.Message}}</p> 9 - <a href="/dashboard" class="w-full flex items-center justify-center gap-2 bg-spot-green text-white rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 7 + <h1 class="text-2xl font-bold text-spot-text mb-1 text-center">{{.Title}}</h1> 8 + <p class="text-spot-secondary text-sm mb-8 text-center">{{.Message}}</p> 9 + <a href="/dashboard" class="w-full flex items-center justify-center gap-2 bg-spot-green text-white rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 10 10 Back to Dashboard 11 11 </a> 12 12 </div>
+31 -28
internal/tmpl/login.html
··· 1 1 {{define "login.html"}} 2 - <div class="max-w-md mx-auto mt-20"> 3 - <div class="bg-spot-surface rounded-xl shadow-spot-heavy p-8"> 4 - <div class="flex justify-center mb-6"> 5 - <a href="/" class="w-14 h-14 block">{{template "logo-icon"}}</a> 2 + <style> 3 + .login-divider { display: flex; align-items: center; gap: 0.75rem; } 4 + .login-divider::before, .login-divider::after { content: ''; flex: 1; height: 1px; background: var(--spot-divider); } 5 + </style> 6 + 7 + <div class="flex items-center justify-center min-h-screen px-4 py-12"> 8 + <div class="max-w-sm w-full"> 9 + <div class="flex justify-center mb-8"> 10 + <a href="/" class="w-16 h-16 block">{{template "logo-icon"}}</a> 6 11 </div> 7 - <h1 class="text-2xl font-bold text-spot-text mb-2 text-center">Sign in to Glean</h1> 8 - <p class="text-spot-secondary text-sm mb-6 text-center">Enter your handle.</p> 12 + 13 + <h1 class="text-2xl font-bold text-spot-text mb-1 text-center">Welcome to Glean</h1> 14 + <p class="text-spot-secondary text-sm mb-8 text-center">The social RSS reader built on AT Protocol.</p> 9 15 10 16 <form action="/auth/start" method="POST" id="login-form"> 11 17 {{csrfInput .CSRFToken}} 12 - <div class="relative mb-5"> 18 + <div class="relative mb-4"> 19 + <label for="handle-input" class="block text-xs text-spot-secondary mb-1.5">Login with your Atmosphere account</label> 13 20 <input type="text" name="handle" placeholder="you.bsky.social" id="handle-input" 14 - class="w-full bg-spot-hover text-spot-text rounded-pill px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder" 21 + class="w-full bg-spot-hover text-spot-text rounded-pill px-5 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder" 15 22 required autocomplete="off"> 16 23 <div id="handle-suggestions" class="absolute left-0 right-0 top-full mt-1 bg-spot-surface border border-spot-divider rounded-xl shadow-spot-heavy z-50 overflow-hidden hidden"></div> 17 24 </div> 18 - </form> 19 25 20 - <div class="space-y-3"> 21 - <button type="submit" form="login-form" 22 - class="w-full flex items-center justify-center gap-3 bg-[#0284FF] text-white rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 23 - <span class="w-5 h-5">{{template "icon-bluesky"}}</span> 24 - Sign in with Bluesky 26 + <button type="submit" 27 + class="w-full flex items-center justify-center bg-spot-green text-white rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 28 + Login 25 29 </button> 30 + </form> 26 31 27 - <button type="submit" form="login-form" 28 - class="w-full flex items-center justify-center gap-3 border border-spot-outline text-spot-text rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition"> 29 - <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg> 30 - Sign in with Atmosphere 31 - </button> 32 + <div class="login-divider text-xs text-spot-muted uppercase tracking-wide mt-8 mb-6"> 33 + <span>New here?</span> 32 34 </div> 33 35 34 - <div class="mt-6 pt-6 border-t border-spot-divider text-center"> 35 - <p class="text-xs text-spot-secondary mb-3">No account yet?</p> 36 - <a href="https://portal.eurosky.tech/create-account" target="_blank" rel="noopener noreferrer" 37 - class="inline-flex items-center justify-center gap-2 w-full border border-spot-outline text-spot-text rounded-pill px-4 py-2.5 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition"> 38 - <span class="text-base">&#127466;&#127482;</span> 39 - Create an account on Eurosky 40 - </a> 41 - </div> 36 + <a href="https://portal.eurosky.tech/create-account" target="_blank" rel="noopener noreferrer" 37 + class="w-full flex items-center justify-center gap-3 border border-spot-outline rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition"> 38 + <svg class="h-5 w-auto" fill="currentColor" viewBox="77.86 113.9 129.15 129.15" xmlns="http://www.w3.org/2000/svg"><path d="M148.846 144.562C148.846 159.75 161.158 172.062 176.346 172.062H207.012V185.865H176.346C161.158 185.865 148.846 198.177 148.846 213.365V243.045H136.029V213.365C136.029 198.177 123.717 185.865 108.529 185.865H77.8633V172.062H108.529C123.717 172.062 136.029 159.75 136.029 144.562V113.896H148.846V144.562Z"/></svg> 39 + Register with Eurosky 40 + </a> 41 + 42 + <p class="text-[11px] text-spot-muted text-center leading-relaxed mt-8"> 43 + By continuing, you agree to the <a href="/terms" class="underline hover:text-spot-secondary transition">Terms of Service</a>. 44 + </p> 42 45 </div> 43 46 </div> 44 47 <script> ··· 119 122 }); 120 123 })(); 121 124 </script> 122 - {{end}} 125 + {{end}}
+72
internal/tmpl/terms.html
··· 1 + {{define "terms.html"}} 2 + <div class="max-w-2xl mx-auto py-12 px-4"> 3 + <h1 class="text-2xl font-bold text-spot-text mb-2">Terms of Service</h1> 4 + <p class="text-sm text-spot-secondary mb-8">Last updated: May 2, 2026</p> 5 + 6 + <div class="space-y-6 text-sm text-spot-body leading-relaxed"> 7 + <div> 8 + <h2 class="text-lg font-bold text-spot-text mb-3">1. Acceptance of Terms</h2> 9 + <p>By accessing or using Glean ("the Service"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Service.</p> 10 + </div> 11 + 12 + <div> 13 + <h2 class="text-lg font-bold text-spot-text mb-3">2. Description of Service</h2> 14 + <p>Glean is a social RSS reader built on the AT Protocol. It allows you to subscribe to RSS and Atom feeds, read articles, highlight passages, leave annotations, and discover new content through personalized recommendations based on your network.</p> 15 + </div> 16 + 17 + <div> 18 + <h2 class="text-lg font-bold text-spot-text mb-3">3. Account and Authentication</h2> 19 + <p>Glean uses AT Protocol identity. You sign in with your handle (e.g. from Bluesky or another AT Protocol account provider). Your subscriptions, annotations, and likes are stored in your personal data repository (PDS) on the AT Protocol. You retain full ownership of your data at all times.</p> 20 + </div> 21 + 22 + <div> 23 + <h2 class="text-lg font-bold text-spot-text mb-3">4. Your Data</h2> 24 + <p>Your data belongs to you. Glean stores records in your PDS using AT Protocol collections. You can export or move your data at any time using standard AT Protocol tools. Glean does not sell, share, or monetize your personal data.</p> 25 + </div> 26 + 27 + <div> 28 + <h2 class="text-lg font-bold text-spot-text mb-3">5. Acceptable Use</h2> 29 + <p>You agree not to use the Service to: violate any applicable law; infringe on the rights of others; distribute spam, malware, or harmful content; attempt to gain unauthorized access to the Service or its infrastructure; or interfere with the proper functioning of the Service.</p> 30 + </div> 31 + 32 + <div> 33 + <h2 class="text-lg font-bold text-spot-text mb-3">6. Content</h2> 34 + <p>Glean indexes publicly available RSS and Atom feeds. We do not host article content. Feed publishers retain all rights to their content. If you are a feed publisher and wish to have your feed removed from our index, please contact us.</p> 35 + </div> 36 + 37 + <div> 38 + <h2 class="text-lg font-bold text-spot-text mb-3">7. Availability</h2> 39 + <p>The Service is provided "as is" and "as available." We strive for reliability but do not guarantee uninterrupted access. We may modify, suspend, or discontinue the Service at any time.</p> 40 + </div> 41 + 42 + <div> 43 + <h2 class="text-lg font-bold text-spot-text mb-3">8. Open Source</h2> 44 + <p>Glean is open source software. You can inspect, fork, and self-host the code. Contributions are welcome subject to the project's license and contribution guidelines.</p> 45 + </div> 46 + 47 + <div> 48 + <h2 class="text-lg font-bold text-spot-text mb-3">9. Limitation of Liability</h2> 49 + <p>To the maximum extent permitted by law, the Service is provided without warranties of any kind. We are not liable for any indirect, incidental, or consequential damages arising from your use of the Service.</p> 50 + </div> 51 + 52 + <div> 53 + <h2 class="text-lg font-bold text-spot-text mb-3">10. Changes to Terms</h2> 54 + <p>We may update these Terms from time to time. Material changes will be communicated through the Service. Continued use of the Service after changes constitutes acceptance of the updated Terms.</p> 55 + </div> 56 + 57 + <div> 58 + <h2 class="text-lg font-bold text-spot-text mb-3">11. Contact</h2> 59 + <p>For questions about these Terms, reach out via <a href="https://bsky.app/profile/glean.at" class="text-spot-green hover:brightness-110 underline">Bluesky</a> or email at <span id="contact-email"></span>.</p> 60 + </div> 61 + </div> 62 + 63 + <div class="mt-12 pt-6 border-t border-spot-divider"> 64 + <a href="/" class="inline-flex items-center justify-center bg-spot-green text-white rounded-pill px-6 py-2.5 text-sm font-bold uppercase tracking-button hover:brightness-110 transition"> 65 + Back to Glean 66 + </a> 67 + </div> 68 + </div> 69 + <script> 70 + (function(){var p='contact',d='glean.at',e=p+'@'+d;var el=document.getElementById('contact-email');var a=document.createElement('a');a.href='mailto:'+e;a.textContent=e;a.className='text-spot-green hover:brightness-110 underline';el.replaceWith(a)})(); 71 + </script> 72 + {{end}}
+3 -3
static/input.css
··· 19 19 --spot-placeholder: rgba(255,255,255,0.45); 20 20 --spot-active-bg: #ffffff; 21 21 --spot-active-text: #1E3932; 22 - --radius-sm: 0.125rem; 23 - --radius-md: 0.375rem; 24 - --radius-lg: 0.5rem; 22 + --radius-sm: 0.5rem; 23 + --radius-md: 0.75rem; 24 + --radius-lg: 1rem; 25 25 --radius-pill: 9999px; 26 26 --radius-full: 50%; 27 27 --spot-shadow: 0 1px 2px rgba(0,0,0,0.40), 0 2px 4px rgba(0,0,0,0.20);