See the best posts from any Bluesky account
0
fork

Configure Feed

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

Add dark mode with cookie-based persistence and system preference detection

Uses a plain cookie readable by both client JS and server middleware so
the server renders the correct .dark class on <html> without flash.
First-time visitors get an inline nonce script that detects
prefers-color-scheme, sets the cookie, and applies dark mode before
paint. A moon/sun toggle (Alpine.js) lets users override manually.

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

+115 -55
+18
app/middleware/theme_middleware.ts
··· 1 + import { type HttpContext } from '@adonisjs/core/http' 2 + import { type NextFn } from '@adonisjs/core/types/http' 3 + 4 + export default class ThemeMiddleware { 5 + handle(ctx: HttpContext, next: NextFn) { 6 + const cookieHeader = ctx.request.header('cookie') || '' 7 + const match = cookieHeader.match(/(?:^|;\s*)theme=(dark|light)/) 8 + const theme = match ? match[1] : '' 9 + 10 + if ('view' in ctx) { 11 + ;(ctx as HttpContext & { view: { share: (data: Record<string, unknown>) => void } }).view.share( 12 + { theme } 13 + ) 14 + } 15 + 16 + return next() 17 + } 18 + }
+1 -1
config/shield.ts
··· 14 14 enabled: true, 15 15 directives: { 16 16 defaultSrc: [`'self'`], 17 - scriptSrc: [`'self'`], 17 + scriptSrc: [`'self'`, `@nonce`], 18 18 styleSrc: [`'self'`, `'unsafe-inline'`, 'https://cdn.jsdelivr.net'], 19 19 imgSrc: [`'self'`, 'data:', 'https://cdn.bsky.app', 'https://video.bsky.app'], 20 20 connectSrc: [`'self'`],
+2
resources/css/app.css
··· 1 1 @import 'tailwindcss'; 2 2 @source '../views'; 3 3 4 + @custom-variant dark (&:where(.dark, .dark *)); 5 + 4 6 @theme { 5 7 /* ── Warm grays — noticeably tinted toward Bluesky blue ── */ 6 8 --color-gray-50: oklch(98% 0.014 255);
+14
resources/js/app.js
··· 1 1 import Alpine from '@alpinejs/csp' 2 2 import { createBackfillProgress } from './backfill_progress.ts' 3 3 4 + Alpine.data('darkMode', function () { 5 + return { 6 + dark: false, 7 + init() { 8 + this.dark = document.documentElement.classList.contains('dark') 9 + }, 10 + toggle() { 11 + this.dark = !this.dark 12 + document.documentElement.classList.toggle('dark', this.dark) 13 + document.cookie = 'theme=' + (this.dark ? 'dark' : 'light') + ';path=/;max-age=31536000;SameSite=Lax' 14 + }, 15 + } 16 + }) 17 + 4 18 Alpine.data('alert', function () { 5 19 return { 6 20 isVisible: false,
+1 -1
resources/views/components/alert/root.edge
··· 1 1 @let(variant = $props.get('variant') || 'default') 2 2 @inject({ variant, autoDismiss: $props.get('autoDismiss') }) 3 3 @let(variantClasses = { 4 - default: 'bg-white border-gray-200', 4 + default: 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700', 5 5 destructive: 'text-red-500 bg-red-500/10 border-red-500', 6 6 success: 'text-green-600 bg-green-600/10 border-green-600', 7 7 })
+1 -1
resources/views/components/button.edge
··· 1 1 @let(buttonTextFromSlot = await $slots.main()) 2 2 @let(buttonText = buttonTextFromSlot.trim() ? buttonTextFromSlot : $props.get('text') ?? '') 3 - @let(classes = ['w-full', 'rounded', 'font-inherit', 'bg-gray-900', 'text-white', 'border-none', 'py-2.5', 'font-medium', 'hover:bg-gray-800', 'cursor-pointer']) 3 + @let(classes = ['w-full', 'rounded', 'font-inherit', 'bg-gray-900', 'dark:bg-gray-100', 'text-white', 'dark:text-gray-900', 'border-none', 'py-2.5', 'font-medium', 'hover:bg-gray-800', 'dark:hover:bg-gray-200', 'cursor-pointer']) 4 4 5 5 <button {{ 6 6 $props.except(['text']).merge({ class: classes }).toAttrs()
+1 -1
resources/views/components/input/control.edge
··· 3 3 ? $context.oldValue || $props.get('value') || '' 4 4 : $props.get('value') || '' 5 5 ) 6 - @let(classes = ['w-full', 'rounded', 'font-inherit', 'h-10', 'border', 'border-gray-300', 'px-4', 'data-[invalid=true]:border-red-500']) 6 + @let(classes = ['w-full', 'rounded', 'font-inherit', 'h-10', 'border', 'border-gray-300', 'dark:border-gray-700', 'dark:bg-gray-900', 'dark:text-gray-100', 'px-4', 'data-[invalid=true]:border-red-500']) 7 7 8 8 <input {{ $props 9 9 .except(['value'])
+31 -17
resources/views/components/layout.edge
··· 1 1 <!DOCTYPE html> 2 - <html lang="en"> 2 + <html lang="en" class="{{ theme === 'dark' ? 'dark' : '' }}"> 3 3 <head> 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <script nonce="{{ cspNonce }}"> 7 + (function(){if(document.cookie.indexOf('theme=')!==-1)return;var d=window.matchMedia('(prefers-color-scheme: dark)').matches;var v=d?'dark':'light';document.cookie='theme='+v+';path=/;max-age=31536000;SameSite=Lax';if(d)document.documentElement.classList.add('dark')})() 8 + </script> 6 9 @vite(['resources/css/app.css', 'resources/js/app.js']) 7 10 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/fill/style.css" /> 8 11 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/bold/style.css" /> ··· 20 23 <link rel="canonical" href="{{ canonicalUrl }}" /> 21 24 @endif 22 25 </head> 23 - <body class="font-sans text-base leading-normal text-gray-900 bg-gray-50"> 26 + <body class="font-sans text-base leading-normal text-gray-900 bg-gray-50 dark:text-gray-100 dark:bg-gray-950"> 24 27 <div class="max-w-[680px] mx-auto px-4"> 25 28 @if(!$props.has('hideHeader')) 26 29 <div class="flex items-center justify-between pt-6 pb-4"> 27 - <a href="/" class="flex items-center gap-1.5 text-gray-900 no-underline hover:opacity-70"> 30 + <a href="/" class="flex items-center gap-1.5 text-gray-900 dark:text-gray-100 no-underline hover:opacity-70"> 28 31 <i class="ph-fill ph-heart text-red-500 text-lg"></i> 29 32 <span class="font-semibold text-sm">skystar</span> 30 33 </a> 31 - <form action="/search" method="GET" class="flex gap-1.5"> 32 - <input 33 - type="text" 34 - name="q" 35 - placeholder="@handle" 36 - class="w-44 px-2.5 py-1.5 text-sm border border-gray-300 rounded-md outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200" 37 - /> 34 + <div class="flex items-center gap-2"> 35 + <form action="/search" method="GET" class="flex gap-1.5"> 36 + <input 37 + type="text" 38 + name="q" 39 + placeholder="@handle" 40 + class="w-44 px-2.5 py-1.5 text-sm border border-gray-300 dark:border-gray-700 rounded-md outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" 41 + /> 42 + <button 43 + type="submit" 44 + class="px-3 py-1.5 text-sm bg-blue-600 text-white border-none rounded-md cursor-pointer hover:bg-blue-700" 45 + >Go</button> 46 + </form> 38 47 <button 39 - type="submit" 40 - class="px-3 py-1.5 text-sm bg-blue-600 text-white border-none rounded-md cursor-pointer hover:bg-blue-700" 41 - >Go</button> 42 - </form> 48 + x-data="darkMode" 49 + x-on:click="toggle" 50 + class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer" 51 + aria-label="Toggle dark mode" 52 + > 53 + <i x-show="!dark" class="ph-fill ph-moon text-base"></i> 54 + <i x-show="dark" class="ph-fill ph-sun text-base"></i> 55 + </button> 56 + </div> 43 57 </div> 44 58 @endif 45 59 {{{ await $slots.main() }}} 46 - <footer class="mt-12 py-6 border-t border-gray-200 text-[13px] text-gray-500 text-center"> 47 - <a href="/" class="text-blue-600 hover:text-blue-700 hover:underline">skystar.social</a> &middot; 48 - <a href="/about" class="text-blue-600 hover:text-blue-700 hover:underline">about</a> 60 + <footer class="mt-12 py-6 border-t border-gray-200 dark:border-gray-800 text-[13px] text-gray-500 dark:text-gray-400 text-center"> 61 + <a href="/" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">skystar.social</a> &middot; 62 + <a href="/about" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">about</a> 49 63 </footer> 50 64 </div> 51 65 </body>
+2 -2
resources/views/components/link.edge
··· 10 10 ) 11 11 : undefined 12 12 ) 13 - @let(activeClass = request.url() === linkHref ? 'text-gray-900' : 'text-gray-700') 14 - @let(classes = ['font-medium', 'hover:text-gray-900', activeClass]) 13 + @let(activeClass = request.url() === linkHref ? 'text-gray-900 dark:text-gray-100' : 'text-gray-700 dark:text-gray-300') 14 + @let(classes = ['font-medium', 'hover:text-gray-900', 'dark:hover:text-gray-100', activeClass]) 15 15 16 16 <a {{ $props 17 17 .except(['href', 'text', 'route', 'routeParams', 'routeOptions'])
+1 -1
resources/views/components/textarea/control.edge
··· 1 - @let(classes = ['w-full', 'rounded', 'font-inherit', 'border', 'border-gray-300', 'px-4', 'py-2', 'data-[invalid=true]:border-red-500']) 1 + @let(classes = ['w-full', 'rounded', 'font-inherit', 'border', 'border-gray-300', 'dark:border-gray-700', 'dark:bg-gray-900', 'dark:text-gray-100', 'px-4', 'py-2', 'data-[invalid=true]:border-red-500']) 2 2 3 3 <textarea {{ $props 4 4 .except(['value'])
+5 -5
resources/views/pages/about.edge
··· 7 7 <div class="py-12 space-y-4"> 8 8 <h1 class="text-3xl font-bold mb-6">About skystar</h1> 9 9 <p> 10 - skystar is a <a href="https://favstar.fm" target="_blank" rel="noopener" class="text-blue-600 hover:underline">favstar.fm</a>-style 11 - site for <a href="https://bsky.app" target="_blank" rel="noopener" class="text-blue-600 hover:underline">Bluesky</a>. 10 + skystar is a <a href="https://favstar.fm" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">favstar.fm</a>-style 11 + site for <a href="https://bsky.app" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">Bluesky</a>. 12 12 Type any handle and see that account's most-liked and most-reposted posts, powered by the 13 - <a href="https://atproto.com" target="_blank" rel="noopener" class="text-blue-600 hover:underline">AT Protocol</a> firehose. 13 + <a href="https://atproto.com" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">AT Protocol</a> firehose. 14 14 </p> 15 15 <p> 16 16 Data is collected from the Bluesky Jetstream feed and updated in near real-time. ··· 22 22 </p> 23 23 <p> 24 24 {{-- TODO: add GitHub URL --}} 25 - Source code: <a href="https://github.com/TODO/skystar" target="_blank" rel="noopener" class="text-blue-600 hover:underline">github.com/TODO/skystar</a> 25 + Source code: <a href="https://github.com/TODO/skystar" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">github.com/TODO/skystar</a> 26 26 </p> 27 - <p><a href="/" class="text-blue-600 hover:underline">← Back to search</a></p> 27 + <p><a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">← Back to search</a></p> 28 28 </div> 29 29 @endslot 30 30 @endcomponent
+1 -1
resources/views/pages/errors/not_found.edge
··· 12 12 Page not found. 13 13 @endif 14 14 </h1> 15 - <p><a href="/" class="text-blue-600 hover:underline">← Try another handle</a></p> 15 + <p><a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">← Try another handle</a></p> 16 16 </div> 17 17 @endslot 18 18 @endcomponent
+1 -1
resources/views/pages/errors/server_error.edge
··· 8 8 <h1 class="text-2xl mb-4"> 9 9 {{ message ?? "We're having a moment, try again in a sec." }} 10 10 </h1> 11 - <p><a href="/" class="text-blue-600 hover:underline">← Back to search</a></p> 11 + <p><a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">← Back to search</a></p> 12 12 </div> 13 13 @endslot 14 14 @endcomponent
+19 -8
resources/views/pages/landing.edge
··· 1 1 @component('components/layout', { hideHeader: true }) 2 2 @slot('main') 3 3 <div class="pt-16 pb-12"> 4 - <h1 class="text-3xl font-bold mb-2">skystar <span class="text-blue-500">✦</span></h1> 5 - <p class="text-lg text-gray-600 mb-10"> 4 + <div class="flex items-center justify-between mb-2"> 5 + <h1 class="text-3xl font-bold">skystar <span class="text-blue-500">✦</span></h1> 6 + <button 7 + x-data="darkMode" 8 + x-on:click="toggle" 9 + class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer" 10 + aria-label="Toggle dark mode" 11 + > 12 + <i x-show="!dark" class="ph-fill ph-moon text-base"></i> 13 + <i x-show="dark" class="ph-fill ph-sun text-base"></i> 14 + </button> 15 + </div> 16 + <p class="text-lg text-gray-600 dark:text-gray-400 mb-10"> 6 17 See any Bluesky account's most-liked and most-reposted posts. 7 18 </p> 8 19 ··· 12 23 name="q" 13 24 placeholder="@handle or handle.bsky.social" 14 25 autofocus 15 - class="flex-1 px-3.5 py-2.5 text-base border border-gray-300 rounded-md outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200" 26 + class="flex-1 px-3.5 py-2.5 text-base border border-gray-300 dark:border-gray-700 rounded-md outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" 16 27 /> 17 28 <button 18 29 type="submit" ··· 23 34 </form> 24 35 25 36 @if(error) 26 - <p class="text-red-700 mb-4 text-[0.95rem]">{{ error }}</p> 37 + <p class="text-red-700 dark:text-red-400 mb-4 text-[0.95rem]">{{ error }}</p> 27 38 @endif 28 39 29 - <p class="text-sm text-gray-400 mb-8"> 40 + <p class="text-sm text-gray-400 dark:text-gray-500 mb-8"> 30 41 Try an example: 31 42 @each(handle in examples) 32 - <a href="/profile/{{ handle }}/likes" class="mr-3 text-blue-600 hover:underline">{{ handle }}</a> 43 + <a href="/profile/{{ handle }}/likes" class="mr-3 text-blue-600 dark:text-blue-400 hover:underline">{{ handle }}</a> 33 44 @endeach 34 45 </p> 35 46 36 - <p class="text-sm text-gray-500 max-w-[520px]"> 47 + <p class="text-sm text-gray-500 dark:text-gray-400 max-w-[520px]"> 37 48 skystar indexes Bluesky posts and tracks engagement from the 38 - <a href="https://atproto.com" target="_blank" rel="noopener" class="text-blue-600 hover:underline">AT Protocol</a> firehose. 49 + <a href="https://atproto.com" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">AT Protocol</a> firehose. 39 50 Type a handle to see that account's greatest hits. 40 51 No login required. 41 52 </p>
+2 -2
resources/views/pages/profile/gone.edge
··· 6 6 @slot('main') 7 7 <div class="py-16 text-center"> 8 8 <h1 class="text-2xl mb-4">{{ '@' + handle }} is no longer available.</h1> 9 - <p class="text-gray-500"> 9 + <p class="text-gray-500 dark:text-gray-400"> 10 10 This account has been deleted or taken down from Bluesky. 11 11 </p> 12 - <p><a href="/" class="text-blue-600 hover:underline">← Back to search</a></p> 12 + <p><a href="/" class="text-blue-600 dark:text-blue-400 hover:underline">← Back to search</a></p> 13 13 </div> 14 14 @endslot 15 15 @endcomponent
+4 -4
resources/views/pages/profile/loading.edge
··· 19 19 <h1 id="backfill-title" class="text-2xl mb-4"> 20 20 Indexing {{ '@' + handle }}… 21 21 </h1> 22 - <p class="text-gray-500 max-w-[420px] mx-auto mb-6"> 22 + <p class="text-gray-500 dark:text-gray-400 max-w-[420px] mx-auto mb-6"> 23 23 This is a one-time index. It may take a few minutes for very active accounts. 24 24 </p> 25 25 <div ··· 28 28 aria-valuemin="0" 29 29 aria-valuemax="{{ totalPosts }}" 30 30 aria-labelledby="backfill-title" 31 - class="w-80 max-w-[80%] h-3.5 mx-auto rounded-full bg-blue-100 overflow-hidden" 31 + class="w-80 max-w-[80%] h-3.5 mx-auto rounded-full bg-blue-100 dark:bg-blue-950 overflow-hidden" 32 32 > 33 33 <div 34 34 class="h-full rounded-full bg-blue-600 transition-[width] duration-500 ease-out" ··· 38 38 <p 39 39 aria-live="polite" 40 40 aria-atomic="true" 41 - class="text-gray-500 mt-3 tabular-nums" 41 + class="text-gray-500 dark:text-gray-400 mt-3 tabular-nums" 42 42 > 43 43 Indexed <span x-text="fetched">{{ fetchedPosts }}</span> of {{ totalPosts }} posts 44 44 </p> ··· 47 47 x-show="state === 'failed'" 48 48 x-cloak 49 49 role="alert" 50 - class="max-w-[420px] mx-auto text-red-700" 50 + class="max-w-[420px] mx-auto text-red-700 dark:text-red-400" 51 51 > 52 52 <h1 class="text-xl mb-3">Backfill failed</h1> 53 53 <p>
+10 -10
resources/views/pages/profile/show.edge
··· 10 10 @if(avatarUrl) 11 11 <img src="{{ avatarUrl }}" alt="" class="size-14 rounded-full shrink-0 object-cover"> 12 12 @else 13 - <div class="size-14 rounded-full bg-gray-300 shrink-0 flex items-center justify-center text-xl text-gray-400">@</div> 13 + <div class="size-14 rounded-full bg-gray-300 dark:bg-gray-700 shrink-0 flex items-center justify-center text-xl text-gray-400">@</div> 14 14 @endif 15 15 <div> 16 16 @if(displayName) 17 17 <div class="text-lg font-semibold group-hover:underline">{{ displayName }}</div> 18 18 @endif 19 - <div class="text-gray-600 group-hover:underline">{{ '@' + handle }}</div> 19 + <div class="text-gray-600 dark:text-gray-400 group-hover:underline">{{ '@' + handle }}</div> 20 20 </div> 21 21 </a> 22 22 ··· 26 26 <div class="flex gap-1"> 27 27 <a 28 28 href="/profile/{{ handle }}/likes{{ daysWindow ? '?days=' + daysWindow : '' }}" 29 - class="px-3.5 py-1.5 rounded-full text-sm {{ kind === 'likes' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300' }}" 29 + class="px-3.5 py-1.5 rounded-full text-sm {{ kind === 'likes' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700' }}" 30 30 >Most liked</a> 31 31 <a 32 32 href="/profile/{{ handle }}/reposts{{ daysWindow ? '?days=' + daysWindow : '' }}" 33 - class="px-3.5 py-1.5 rounded-full text-sm {{ kind === 'reposts' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300' }}" 33 + class="px-3.5 py-1.5 rounded-full text-sm {{ kind === 'reposts' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700' }}" 34 34 >Most reposted</a> 35 35 </div> 36 36 ··· 38 38 <div class="flex gap-1"> 39 39 <a 40 40 href="/profile/{{ handle }}/{{ kind }}" 41 - class="px-3.5 py-1.5 rounded-full text-sm {{ !daysWindow ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300' }}" 41 + class="px-3.5 py-1.5 rounded-full text-sm {{ !daysWindow ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700' }}" 42 42 >All time</a> 43 43 <a 44 44 href="/profile/{{ handle }}/{{ kind }}?days=30" 45 - class="px-3.5 py-1.5 rounded-full text-sm {{ daysWindow === 30 ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300' }}" 45 + class="px-3.5 py-1.5 rounded-full text-sm {{ daysWindow === 30 ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700' }}" 46 46 >Last month</a> 47 47 </div> 48 48 </div> 49 49 50 50 {{-- Truncation notice --}} 51 51 @if(indexedSince) 52 - <p class="text-sm text-gray-600 mb-4"> 52 + <p class="text-sm text-gray-600 dark:text-gray-400 mb-4"> 53 53 This is a prolific poster! Showing posts since {{ indexedSince.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}. 54 54 Older posts were not indexed. 55 55 </p> ··· 61 61 @else 62 62 <ol class="list-none p-0 m-0"> 63 63 @each((post, index) in posts) 64 - <li class="{{ index === 0 ? (kind === 'likes' ? 'bg-amber-50 border-amber-200' : 'bg-blue-50 border-blue-200') : 'bg-white border-gray-200' }} border rounded-lg p-4 mb-3"> 64 + <li class="{{ index === 0 ? (kind === 'likes' ? 'bg-amber-50 dark:bg-amber-200/10 border-amber-200 dark:border-amber-200/30' : 'bg-blue-50 dark:bg-blue-200/10 border-blue-200 dark:border-blue-200/30') : 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800' }} border rounded-lg p-4 mb-3"> 65 65 <div class="flex items-start gap-3"> 66 66 <div class="text-sm font-semibold text-gray-400 pt-0.5 shrink-0 tabular-nums min-w-4 text-right">{{ index + 1 }}</div> 67 67 <div class="flex-1 min-w-0"> ··· 151 151 </div> 152 152 @elseif(post.embed.type === 'external') 153 153 <div class="mb-3"> 154 - <a href="{{ post.embed.uri }}" target="_blank" rel="noopener" class="flex border border-neutral-200 rounded-lg overflow-hidden no-underline text-inherit"> 154 + <a href="{{ post.embed.uri }}" target="_blank" rel="noopener" class="flex border border-neutral-200 dark:border-gray-700 rounded-lg overflow-hidden no-underline text-inherit"> 155 155 @if(post.embed.thumb) 156 156 <div class="shrink-0 w-[120px] min-h-[80px]"> 157 157 <img ··· 164 164 @endif 165 165 <div class="py-2.5 px-3 min-w-0 flex-1"> 166 166 <div class="font-semibold text-sm mb-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ post.embed.title }}</div> 167 - <div class="text-xs text-gray-400 line-clamp-2">{{ post.embed.description }}</div> 167 + <div class="text-xs text-gray-400 dark:text-gray-500 line-clamp-2">{{ post.embed.description }}</div> 168 168 </div> 169 169 </a> 170 170 </div>
+1
start/kernel.ts
··· 24 24 */ 25 25 server.use([ 26 26 () => import('#middleware/container_bindings_middleware'), 27 + () => import('#middleware/theme_middleware'), 27 28 () => import('@adonisjs/static/static_middleware'), 28 29 () => import('@adonisjs/vite/vite_middleware'), 29 30 ])