Anime Discovery Platform – Next.js, GraphQL & REST, Server-Side Rendering FullStack Fundamental Project 2 (with infinite scroll, server actions)
A modern, server-rendered anime discovery platform built with Next.js 15 App Router. Browse anime with infinite scroll, search and filters (kind, score, status, order), and click through to full detail pages. Data is fetched via Shikimori GraphQL for the list (and detail when available), with REST fallback for single-anime detail. Poster and screenshots open in a 90vh lightbox with download; video thumbnails and Lucide icons are used across the UI. No backend, database, or auth—ideal for learning Server Components, Server Actions, GraphQL, and SEO.
Live Demo: https://anime-lover.vercel.app/
- Project Overview
- Features
- Technology Stack
- Project Structure
- Getting Started
- Environment Variables & .env
- How to Run
- Routes & Pages
- Project Walkthrough & Data Flow
- Components Documentation
- API Integration & Backend
- Key Functionalities
- Code Examples
- Reusing Components in Other Projects
- Deployment
- Keywords
- Conclusion
- License
Anime Vault is a production-ready anime discovery platform built with Next.js 15 App Router. It is intended for educational use—teaching Server Components, Client Components, Server Actions, GraphQL, REST fallback, infinite scroll, and SEO in one codebase.
What this project showcases:
- Server-Side Rendering (SSR) — Initial HTML is rendered on the server for fast loads and better SEO.
- Server Actions — Server-side functions called from the client (e.g.
fetchAnime) without custom API routes for data. - GraphQL + REST — List and detail use Shikimori GraphQL; detail falls back to REST when GraphQL returns no data.
- Infinite Scroll — More anime load as you scroll via the Intersection Observer API.
- Search & Filters — Kind, score, status, order; state in URL for shareable links.
- Detail Pages — Per-anime route with poster, metadata, genres, studios, videos, screenshots, and lightbox + download.
- Framer Motion & Lucide — Staggered card animations and Lucide React icons for badges and actions.
- TypeScript & Tailwind — Full type safety and utility-first styling.
No backend database or auth; data comes from the public Shikimori API. Suitable for learning Next.js 15, GraphQL, and reusing patterns in other projects.
- 🎨 Modern UI/UX — Dark-themed interface with smooth animations and Lucide icons.
- 🏠 Home — Anime grid with infinite scroll, search bar, and filters (Kind, Score, Status, Order).
- 📄 Detail Pages — Full anime info: poster (clickable → 90vh lightbox + download), score, episodes, genres, studios, description, videos, screenshots (clickable → lightbox + download), next episode, external links.
- 🔄 Infinite Scroll — Automatic loading of more anime as you scroll; respects current filters.
- 🚀 Server-Side Rendering — Fast first paint and SEO-friendly HTML; dynamic metadata per anime.
- 📱 Fully Responsive — Works on desktop, tablet, and mobile.
- 🎭 Framer Motion — Staggered card animations on the home grid.
- 🔍 Real-Time Data — Shikimori GraphQL for list (and detail when supported); REST fallback for detail.
- ⚡ Images — Next.js
Imagewhere possible; poster/screenshots lightbox with download via API proxy. - 📊 SEO — Metadata, Open Graph, Twitter cards, web manifest.
- Next.js 15 App Router:
/(home),/anime/[id](detail),/api/download-image(image download proxy). - Server Actions for list fetching; GraphQL in
lib/shikimori-graphql.ts; REST fallback for single anime. - Client Components:
LoadMore,SearchFilters,BackToTop,PosterLightbox,ScreenshotsGallery,Motion. - Filters and search reflected in URL; Back to top button after scroll.
- PWA-ready manifest and theme color.
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15 | App Router, SSR, Server Actions, API routes |
| React | 18 | UI library and component model |
| TypeScript | 5 | Static typing and better DX |
| Tailwind CSS | 3 | Utility-first CSS and responsive design |
- framer-motion — Declarative animations; used in
AnimeCardviaMotionwrapper. - react-intersection-observer —
useInViewfor infinite-scroll trigger. - lucide-react — Icons for badges (kind, status, score, episodes), download, close, etc.
- ESLint + eslint-config-next — Linting (
npm run lint). - PostCSS + Autoprefixer — Tailwind.
- Shikimori — GraphQL for list (and detail when supported); REST fallback for single-anime detail. No API key. GraphQL · API.
anime-vault/
├── app/
│ ├── layout.tsx # Root layout, metadata, Hero, Footer, Providers
│ ├── page.tsx # Home: grid, SearchFilters, LoadMore, BackToTop
│ ├── action.tsx # Server Action fetchAnime (GraphQL list)
│ ├── providers.tsx # Client providers wrapper
│ ├── globals.css # Tailwind + global styles
│ ├── anime/[id]/
│ │ ├── page.tsx # Detail page (GraphQL → REST fallback, PosterLightbox, ScreenshotsGallery)
│ │ ├── loading.tsx # Skeleton while loading
│ │ └── not-found.tsx # 404 for missing anime
│ └── api/download-image/
│ └── route.ts # GET proxy for image download (same-origin)
│
├── components/
│ ├── AnimeCard.tsx # Card with Lucide icons (kind, status, score, episodes)
│ ├── LoadMore.tsx # Infinite scroll + fetchAnime with filters
│ ├── SearchFilters.tsx # Search + Kind, Score, Status, Order dropdowns
│ ├── BackToTop.tsx # Fixed button after scroll
│ ├── PosterLightbox.tsx # Clickable poster → 90vh modal + download
│ ├── ScreenshotsGallery.tsx # Clickable screenshots → modal + download
│ ├── Hero.tsx, Footer.tsx # Header and footer
│ ├── Motion.tsx # Framer Motion wrapper
│ └── ui/ # Input, Select, Button
│
├── lib/
│ ├── api.ts # SHIKIMORI_BASE, ANIMES_API, GRAPHQL_ENDPOINT, MAX_ANIME_LIMIT
│ ├── shikimori-graphql.ts # GraphQL client, list/detail queries, REST fallback
│ ├── filter-options.ts # KIND_OPTIONS, STATUS_OPTIONS, SCORE_OPTIONS, ORDER_OPTIONS
│ └── download-image.ts # downloadImage(url, filename) → API proxy
│
├── types/
│ └── anime.ts # AnimeProp, AnimeFilters, AnimeDetail
│
├── hooks/
│ └── useFiltersFromSearchParams.ts
│
├── public/ # Static assets (logo, icons, manifest, etc.)
├── next.config.js # images.remotePatterns (shikimori, img.youtube, etc.)
├── tailwind.config.ts
├── tsconfig.json
└── package.jsonImportant files:
app/layout.tsx— Global metadata, viewport, fonts; Hero + children + Footer.app/page.tsx— Home: reads URL params → filters, fetches first page via GraphQL, renders grid + SearchFilters + LoadMore + BackToTop.app/action.tsx—fetchAnime(page, filters)Server Action; callsfetchAnimeListGraphQL.app/anime/[id]/page.tsx— Detail:fetchAnimeByIdGraphQLthenfetchAnimeByIdRESTfallback; poster lightbox, videos, screenshots gallery, Lucide badges.lib/shikimori-graphql.ts— GraphQL requests, list/detail normalization, REST fallback for detail.components/AnimeCard.tsx— Single card (Lucide icons); links to/anime/[id].components/LoadMore.tsx— Client:useInView+fetchAnime(page, filters).
- Node.js 18.x or later.
- npm, yarn, or pnpm.
git clone https://github.com/yourusername/anime-vault.git
cd anime-vault
npm installThen run the app (see How to Run). No environment variables are required to run the project; the Shikimori API is public and used without authentication.
You do not need any environment variables to run this project. The Shikimori API is public and does not require an API key. The app works out of the box after npm install and npm run dev.
Optional: If you later add features that need configuration (e.g. a custom API base URL or analytics), you can use a .env.local file:
- Create
.env.localin the project root (do not commit it). - Add variables. Use the
NEXT_PUBLIC_prefix only for values that must be available in the browser.
Example (optional, not required for current features):
# Optional: override API base (current code uses hardcoded Shikimori URL)
# NEXT_PUBLIC_API_URL=https://shikimori.one/api- Access in code:
- Server (Server Components, Server Actions):
process.env.NEXT_PUBLIC_API_URL - Client: same; only
NEXT_PUBLIC_*is exposed to the client.
- Server (Server Components, Server Actions):
Summary: No .env is required. If you add one for optional settings, keep secrets server-side and document any new variables in this README.
npm run devStarts the dev server at http://localhost:3000 with hot reload.
npm run build
npm startBuilds and runs the app in production mode.
npm run lintRuns ESLint (Next.js config) to check code quality.
| Route | File | Description |
|---|---|---|
/ |
app/page.tsx |
Home: search, filters, anime grid, infinite scroll, Back to top |
/anime/[id] |
app/anime/[id]/page.tsx |
Detail: poster (lightbox + download), metadata, videos, screenshots (lightbox + download) |
/api/download-image |
app/api/download-image/route.ts |
GET proxy for image download (allowed hosts: shikimori, img.youtube) |
List data is fetched via the Server Action fetchAnime in app/action.tsx (GraphQL). Detail data is fetched in app/anime/[id]/page.tsx (GraphQL with REST fallback).
- Server Components (default) —
layout.tsx,page.tsx,Hero,Footer,AnimeCard(when rendered from server). No"use client". - Client Components —
LoadMore.tsxandMotion.tsxuse"use client"for hooks and browser APIs. - Server Actions —
fetchAnimeinaction.tsxruns on the server and can be called from the client.
Home: User visits / (optionally with ?search=...&kind=...&status=...&order=...) → page.tsx reads params → fetchAnime(1, filters) (GraphQL list) → first 8 cards rendered → HTML sent → LoadMore and SearchFilters hydrate → scroll triggers fetchAnime(2, filters), etc.
Detail: User clicks a card → /anime/[id] → fetchAnimeByIdGraphQL(id); if empty, fetchAnimeByIdREST(id) → poster, metadata, videos, screenshots, links rendered. Poster and screenshots open in lightbox with download via /api/download-image.
- Server Components reduce client JS and improve first load.
- Server Actions replace the need for
/apiroutes for this use case. - Infinite scroll is implemented with a single Client Component and one Server Action.
File: components/AnimeCard.tsx
Type: Client (uses MotionDiv; card is a Link to /anime/[id]).
Purpose: Renders one anime: poster, name, status + kind badges (Lucide), up to 2 genres, episodes (Clapperboard), score (Star).
Props: anime: AnimeProp, index: number. AnimeProp includes optional genres, status, rating from GraphQL.
Behavior: MotionDiv for fade-in; image.original is already a full URL from GraphQL mapping. Lucide icons: Tv/Clapperboard for kind, PlayCircle/CalendarCheck for status, Star, Clapperboard for score/episodes.
Usage: Rendered by fetchAnime in action.tsx; each card links to /anime/${anime.id}.
File: components/LoadMore.tsx
Type: Client Component ("use client").
Purpose: Renders an extra grid of anime cards and a “trigger” div. When the trigger enters the viewport, it fetches the next page and appends new cards.
Behavior: Uses useInView from react-intersection-observer. On inView, calls fetchAnime(page) (Server Action). Keeps a module-level page (starts at 2). Shows a spinner while loading. Uses a short delay (500 ms) before requesting to avoid rapid repeated calls.
Usage: In app/page.tsx, below the grid; pass key={JSON.stringify(filters)} and initialFilters={filters} so pagination resets when filters change.
<LoadMore
key={JSON.stringify(filters ?? {})}
initialFilters={filters ?? undefined}
/>File: components/Hero.tsx
Type: Server Component.
Purpose: Top section: logo, headline (“Explore The Diverse Realms of Anime Magic”), and hero image. Uses Tailwind bg-hero (from tailwind.config.ts) for background.
Usage: Rendered in app/layout.tsx above {children}.
File: components/Footer.tsx
Type: Server Component.
Purpose: Footer with copyright text, logo, and social icons (Twitter, Instagram, TikTok).
Usage: Rendered in app/layout.tsx below {children}.
File: components/SearchFilters.tsx
Type: Client Component.
Purpose: Search input + Kind, Score, Status, Order dropdowns; updates URL search params so the home page and LoadMore use the same filters. “Clear filters” resets to /.
File: components/BackToTop.tsx
Type: Client Component.
Purpose: Fixed bottom-right “Back to top” button; visible after scrolling ~400px; smooth scroll to top. Rendered once on the home page.
File: components/PosterLightbox.tsx
Type: Client Component.
Purpose: Clickable poster on the detail page; opens a 90vh modal with the image and Download / Close (Lucide). Download uses downloadImage() → API proxy so the file is saved. Escape or click outside to close.
File: components/ScreenshotsGallery.tsx
Type: Client Component.
Purpose: Grid of clickable screenshot thumbnails; clicking opens a 90vh modal with the image and Download / Close. Same download behavior as PosterLightbox.
File: components/Motion.tsx
Type: Client Component ("use client").
Purpose: Re-exports motion.div from Framer Motion so the rest of the app can use animations without importing Framer in every file.
Usage:
import { MotionDiv } from "@/components/Motion";
<MotionDiv
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1 } }}
initial="hidden"
animate="visible"
transition={{ delay: 0.25 }}
>
Content
</MotionDiv>;This project uses Shikimori’s GraphQL API for list and detail data (single endpoint, request only the fields you need). Queries, variables, and response handling live in lib/shikimori-graphql.ts—see that file for the full list/detail queries and how variables map to filters (search, kind, score, status, order). You can try queries in the Shikimori GraphQL playground.
- Endpoint:
POST https://shikimori.one/api/graphql - Auth: None. Public API. Optional
User-Agent(e.g.AnimeVault/1.0) is sent.
Where it’s used:
- List:
app/action.tsx→fetchAnimeListGraphQL(page, filters)→ renders<AnimeCard />for each item. - Detail:
app/anime/[id]/page.tsx→fetchAnimeByIdGraphQL(id)first; if that returns nothing, REST fallbackfetchAnimeByIdREST(id)(GEThttps://shikimori.one/api/animes/:id). Detail page shows poster, metadata, genres, studios, description, videos (thumbnails unoptimized for any host), screenshots, next episode, external links.
Image download: For poster and screenshot lightboxes, the Download button uses /api/download-image?url=...&filename=... so the browser gets a same-origin response with Content-Disposition: attachment and saves the file instead of opening in a new tab. Allowed hosts: shikimori.one, shikimori.io, img.youtube.com.
There is no separate backend; the Next.js server runs Server Actions, the detail page, and the download-image API route.
The home page is a Server Component. fetchAnime(1) runs on the server; the first 8 anime are rendered to HTML and sent to the client. Benefits: smaller client bundle, better SEO, fast first paint.
"use server" in action.tsx makes fetchAnime a Server Action. It can be called from the client (e.g. LoadMore) without defining a route. Arguments and return values are serialized automatically.
LoadMore uses useInView to detect when a trigger element is visible, then calls fetchAnime(page) and appends the returned React elements to local state. The same grid layout is used for initial (server) and loaded (client) cards.
AnimeCard uses MotionDiv with simple opacity variants and a stagger delay based on index. Animations run when the component mounts (and optionally when in view, depending on viewport props).
Next.js Image is used for posters and screenshots; next.config.js images.remotePatterns allows Shikimori and img.youtube (http/https). Video thumbnails use unoptimized so any host (e.g. video.sibnet.ru) works without adding every domain. Poster and screenshot downloads go through /api/download-image so the response is same-origin and the browser saves the file instead of opening in a new tab.
"use client";
import { fetchAnime } from "@/app/action";
export function MyLoader() {
const [cards, setCards] = useState([]);
const load = async () => {
const next = await fetchAnime(2);
setCards((prev) => [...prev, ...next]);
};
return <button onClick={load}>Load more</button>;
}import type { AnimeProp } from "@/components/AnimeCard";
const anime: AnimeProp = {
id: "1",
name: "Naruto",
image: { original: "https://shikimori.io/.../1.jpg" }, // full URL from GraphQL mapping
kind: "TV",
episodes: 220,
episodes_aired: 220,
score: "8.3",
genres: [{ id: 1, name: "Adventure" }], // optional
status: "released", // optional
};Create app/about/page.tsx:
export default function About() {
return (
<div className="p-8 text-white">
<h1>About Anime Vault</h1>
<p>Learning project for Next.js 14 App Router.</p>
</div>
);
}The page is available at /about. Layout (Hero + Footer) is shared automatically.
- Copy
AnimeCard.tsxandMotion.tsx. - Install:
framer-motion,next. - Adjust the
AnimePropinterface to match your data (e.g. different field names or image structure). - Update image
srcif you use another API (e.g. full URL instead ofhttps://shikimori.one${...}). - Reuse:
<AnimeCard anime={item} index={i} />.
- Copy
LoadMore.tsx. - Install:
react-intersection-observer. - Replace
fetchAnimewith your own Server Action or async function that returns an array of React nodes (or items you map to cards). - Adjust the grid classes and loading UI to match your layout.
- Ensure the parent passes or shares page state if you need a single source of truth.
Copy the component files and any assets they reference (e.g. logo.svg, hero.png). Update text, links, and images to fit your project. Both are Server Components and need no extra dependencies beyond Next.js and Tailwind.
Use Motion.tsx anywhere you need Framer Motion’s motion.div with a consistent import path. You can add motion.section, motion.span, etc. in the same file and re-export them.
- Push the repo to GitHub.
- In Vercel, import the repo and deploy. Use default Next.js settings (build:
next build, output: default). - No env vars are required for the current Shikimori-based flow.
Run npm run build and then npm start. Set the Node version to 18+ if the platform allows. Static export is not used; the app uses Server Components and Server Actions and needs a Node server.
Anime, anime list, anime browser, anime database, Anime Vault, Japanese animation, anime discovery, anime ratings, Shikimori, Next.js 15, React 18, TypeScript, Server Components, Client Components, Server Actions, App Router, infinite scroll, Framer Motion, Tailwind CSS, SSR, SEO, responsive design, learning project, open source.
Anime Vault is a full example of a Next.js 15 App Router app: Server Components for layout and pages, Server Actions for list data, GraphQL + REST for Shikimori, and Client Components for filters, infinite scroll, lightboxes, and Back to top. No env vars are required; the Shikimori API is public.
Takeaways:
- Server Components reduce client JS and improve SEO; detail pages use dynamic metadata per anime.
- Server Actions power list fetching; GraphQL is used for list and detail with REST fallback when GraphQL returns no detail.
- Infinite scroll plus URL-based filters (search, kind, score, status, order) keep state shareable.
- Image download for cross-origin URLs can be handled with a same-origin API proxy and
Content-Disposition: attachment. - Lucide React and Framer Motion keep the UI consistent and animated.
Possible next steps: Favorites (e.g. localStorage), dark/light theme, or another data source while reusing the same patterns.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project — feel free to use, enhance, and extend it further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.
Enjoy building and learning! 🚀
Thank you! 😊



