perf: optimize page load with async HTMX fragments

This commit is contained in:
2026-05-05 16:05:51 +02:00
parent cb16d8e6c7
commit c50258c476
4 changed files with 142 additions and 63 deletions

View File

@@ -1,3 +1,43 @@
{{define "anime_characters"}}
<div class="mt-12 w-full">
<h2 class="mb-6 text-lg font-normal text-neutral-300">Characters & Cast</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{{range (slice . 0 (min (len .) 10))}}
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5">
<div class="h-16 w-12 shrink-0 overflow-hidden bg-white/5">
<img src="{{.Character.Images.Jpg.ImageURL}}" alt="{{.Character.Name}}" class="h-full w-full object-cover" loading="lazy" />
</div>
<div class="flex flex-col justify-center overflow-hidden">
<span class="truncate text-sm font-medium text-neutral-200">{{.Character.Name}}</span>
<span class="truncate text-xs text-neutral-500">{{.Role}}</span>
{{if .VoiceActors}}
<span class="mt-1 truncate text-[11px] text-neutral-400">{{(index .VoiceActors 0).Person.Name}}</span>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{define "anime_recommendations"}}
{{if .}}
<div class="w-full">
<h2 class="mb-6 text-lg font-normal text-neutral-300">Recommendations</h2>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
{{range (slice . 0 (min (len .) 8))}}
<a href="/anime/{{.Entry.MalID}}" class="group flex flex-col gap-2">
<div class="aspect-2/3 overflow-hidden bg-white/5 shadow-md">
<img src="{{.Entry.Images.Webp.LargeImageURL}}" alt="{{.Entry.Title}}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
</div>
<span class="truncate text-xs font-medium text-neutral-400 transition-colors group-hover:text-white">{{.Entry.Title}}</span>
</a>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{define "title"}}{{.Anime.DisplayTitle}}{{end}}
{{define "content"}}
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
@@ -201,27 +241,22 @@
</div>
</div>
{{if .Characters}}
<div hx-get="/anime/{{$anime.MalID}}?section=characters" hx-trigger="load" hx-swap="outerHTML">
<div class="mt-12 w-full">
<h2 class="mb-6 text-lg font-normal text-neutral-300">Characters & Cast</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{{range (slice .Characters 0 (min (len .Characters) 10))}}
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5">
<div class="h-16 w-12 shrink-0 overflow-hidden bg-white/5">
<img src="{{.Character.Images.Jpg.ImageURL}}" alt="{{.Character.Name}}" class="h-full w-full object-cover" loading="lazy" />
</div>
<div class="flex flex-col justify-center overflow-hidden">
<span class="truncate text-sm font-medium text-neutral-200">{{.Character.Name}}</span>
<span class="truncate text-xs text-neutral-500">{{.Role}}</span>
{{if .VoiceActors}}
<span class="mt-1 truncate text-[11px] text-neutral-400">{{(index .VoiceActors 0).Person.Name}}</span>
{{end}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 animate-pulse">
{{range (seq 5)}}
<div class="flex gap-3 bg-white/[0.02] p-3 ring-1 ring-white/5 h-20">
<div class="h-16 w-12 shrink-0 bg-white/5"></div>
<div class="flex flex-col justify-center gap-2 grow">
<div class="h-3 w-2/3 bg-white/5 rounded"></div>
<div class="h-2 w-1/2 bg-white/5 rounded"></div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
<div class="w-full">
<div hx-get="/api/watch-order?animeId={{$anime.MalID}}" hx-trigger="load">
@@ -232,20 +267,19 @@
</div>
</div>
{{if .Recommendations}}
<div class="mt-12 w-full">
<div hx-get="/anime/{{$anime.MalID}}?section=recommendations" hx-trigger="load" hx-swap="outerHTML">
<div class="w-full">
<h2 class="mb-6 text-lg font-normal text-neutral-300">Recommendations</h2>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
{{range (slice .Recommendations 0 (min (len .Recommendations) 8))}}
<a href="/anime/{{.Entry.MalID}}" class="group flex flex-col gap-2">
<div class="aspect-2/3 overflow-hidden bg-white/5 shadow-md">
<img src="{{.Entry.Images.Webp.LargeImageURL}}" alt="{{.Entry.Title}}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
</div>
<span class="truncate text-xs font-medium text-neutral-400 transition-colors group-hover:text-white">{{.Entry.Title}}</span>
</a>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 animate-pulse">
{{range (seq 8)}}
<div class="flex flex-col gap-2">
<div class="aspect-2/3 bg-white/5"></div>
<div class="h-3 w-full bg-white/5 rounded"></div>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
{{end}}

View File

@@ -14,10 +14,7 @@
{{$imageUrl = $anime.Images.Jpg.LargeImageURL}}
{{end}}
{{$displayTitle := $anime.Title}}
{{if $anime.TitleEnglish}}
{{$displayTitle = $anime.TitleEnglish}}
{{end}}
{{$displayTitle := $anime.DisplayTitle}}
<div class="flex w-full flex-col gap-2" data-id="{{$dataId}}">
<a href="/anime/{{$anime.MalID}}" class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 {{if $withActions}}after:bg-black/80 after:opacity-0 hover:after:opacity-100{{else}}after:bg-linear-to-t after:from-black/80 after:via-black/20 after:to-transparent after:opacity-80 hover:after:opacity-100{{end}} after:transition-opacity">

View File

@@ -1,6 +1,5 @@
{{define "title"}}Discover{{end}}
{{define "content"}}
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
<script>
let isFetchingRandom = false;
@@ -51,45 +50,36 @@
</div>
</section>
{{/* Trending Section */}}
<section class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-white">Trending This Season</h2>
<a href="/browse?order_by=popularity&sort=desc" class="text-accent text-sm hover:underline">View all</a>
</div>
<div class="discover-grid">
{{range $i, $anime := .Trending}}
<div>
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
</div>
{{end}}
<div hx-get="/api/discover/trending" hx-trigger="load" hx-swap="outerHTML">
{{template "discover_skeleton"}}
</div>
</section>
{{/* Upcoming Section */}}
<section class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-white">Highly Anticipated</h2>
<a href="/browse?status=upcoming&order_by=members&sort=desc" class="text-accent text-sm hover:underline">View all</a>
</div>
<div class="discover-grid">
{{range $i, $anime := .Upcoming}}
<div>
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
</div>
{{end}}
<div hx-get="/api/discover/upcoming" hx-trigger="load" hx-swap="outerHTML">
{{template "discover_skeleton"}}
</div>
</section>
{{/* Top Section */}}
<section class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-white">All-Time Greats</h2>
<a href="/browse?order_by=score&sort=desc" class="text-accent text-sm hover:underline">View all</a>
</div>
<div class="discover-grid">
{{range $i, $anime := .Top}}
<div>
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
</div>
{{end}}
<div hx-get="/api/discover/top" hx-trigger="load" hx-swap="outerHTML">
{{template "discover_skeleton"}}
</div>
</section>
</div>
@@ -100,6 +90,34 @@
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
}
.skeleton {
background: linear-gradient(90deg, #1f1f1f 25%, #2a2a2a 50%, #1f1f1f 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
{{end}}
{{end}}
{{define "discover_section"}}
<div class="discover-grid">
{{range .Animes}}
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
{{end}}
</div>
{{end}}
{{define "discover_skeleton"}}
<div class="discover-grid">
{{range (seq 8)}}
<div class="flex flex-col gap-2">
<div class="skeleton aspect-[2/3] w-full rounded-xl"></div>
<div class="skeleton h-4 w-3/4 rounded"></div>
<div class="skeleton h-3 w-1/2 rounded opacity-50"></div>
</div>
{{end}}
</div>
{{end}}

View File

@@ -1,9 +1,8 @@
{{define "title"}}Home{{end}}
{{define "content"}}
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
<div class="flex flex-col gap-10">
{{if .ContinueWatching}}
{{template "continue_watching" .ContinueWatching}}
{{if .User}}
<div hx-get="/api/catalog/continue" hx-trigger="load" hx-swap="outerHTML"></div>
{{end}}
<section class="w-full">
@@ -14,11 +13,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="transition-transform group-hover:translate-x-0.5"><path d="m9 18 6-6-6-6"/></svg>
</a>
</div>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range .CurrentlyAiring}}
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
{{end}}
<div hx-get="/api/catalog/airing" hx-trigger="load" hx-swap="outerHTML">
{{template "catalog_skeleton"}}
</div>
</section>
@@ -30,12 +26,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="transition-transform group-hover:translate-x-0.5"><path d="m9 18 6-6-6-6"/></svg>
</a>
</div>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range .MostPopular}}
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
{{end}}
<div hx-get="/api/catalog/popular" hx-trigger="load" hx-swap="outerHTML">
{{template "catalog_skeleton"}}
</div>
</section>
</div>
{{end}}
<style>
.skeleton {
background: linear-gradient(90deg, #1f1f1f 25%, #2a2a2a 50%, #1f1f1f 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
{{end}}
{{define "catalog_section"}}
{{if eq .Section "Continue"}}
{{if .ContinueWatching}}
{{template "continue_watching" .ContinueWatching}}
{{end}}
{{else}}
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range .Animes}}
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
{{end}}
</div>
{{end}}
{{end}}
{{define "catalog_skeleton"}}
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range (seq 6)}}
<div class="flex flex-col gap-2">
<div class="skeleton aspect-[2/3] w-full rounded-xl"></div>
<div class="skeleton h-4 w-3/4 rounded"></div>
</div>
{{end}}
</div>
{{end}}