Add hello world landing page and Docker deployment config
Simple landing page for initial Portainer GitOps deployment. Includes Dockerfile, docker-compose.yml with pull_policy: build, and project documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7374493c5
commit
9bd75381a7
162
AGENTS.md
162
AGENTS.md
@ -1,5 +1,163 @@
|
|||||||
<!-- BEGIN:nextjs-agent-rules -->
|
|
||||||
# This is NOT the Next.js you know
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
<!-- END:nextjs-agent-rules -->
|
|
||||||
|
# Manga Website Project Requirements
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Frontend & Backend | Next.js (App Router, TypeScript, Tailwind CSS) |
|
||||||
|
| Database | PostgreSQL 16 |
|
||||||
|
| ORM | Prisma |
|
||||||
|
| Image Storage | Cloudflare R2 |
|
||||||
|
| Containerization | Docker + Docker Compose (managed via Portainer) |
|
||||||
|
| Reverse Proxy | Nginx via aaPanel |
|
||||||
|
| Domain | `manga.04080616.xyz` |
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
- **Server**: Proxmox host, Docker VM/LXC running Portainer
|
||||||
|
- **Reverse proxy**: aaPanel Nginx routes `manga.04080616.xyz` → `http://127.0.0.1:3001`
|
||||||
|
- **SSL**: Let's Encrypt via aaPanel
|
||||||
|
- **DDNS already configured** — no Cloudflare Tunnel needed
|
||||||
|
|
||||||
|
### Port Allocation
|
||||||
|
|
||||||
|
| Port | Service |
|
||||||
|
|------|---------|
|
||||||
|
| 3001 | Next.js app (host) → 3000 (container) |
|
||||||
|
| 5433 | PostgreSQL (host) → 5432 (container) |
|
||||||
|
|
||||||
|
Ports 3000, 8000–8002, 8005, 2283, 5432, 51820–51821, 9443 are already in use.
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
1. Develop on macOS locally
|
||||||
|
2. Push code to Gitea (`gitea.04080616.xyz/yiekheng/sunnymh-manga-site`)
|
||||||
|
3. On Proxmox Docker host: `git pull` → `docker build` → restart container
|
||||||
|
4. Portainer used for container management (GUI)
|
||||||
|
|
||||||
|
## Image Storage (Cloudflare R2)
|
||||||
|
|
||||||
|
- Images stored in R2 (S3-compatible), **not** on the Proxmox host
|
||||||
|
- Upload flow: backend generates presigned URL → browser uploads directly to R2 → database records image path
|
||||||
|
- Format: **WebP** (25–35% smaller than JPEG)
|
||||||
|
- Multiple resolutions: thumbnail / reading / original
|
||||||
|
|
||||||
|
### R2 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/manga/{manga_id}/{chapter_id}/{page_number}.webp
|
||||||
|
/thumbnails/{manga_id}/cover.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema (Prisma)
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Manga {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
description String
|
||||||
|
coverUrl String
|
||||||
|
slug String @unique
|
||||||
|
status Status @default(PUBLISHED)
|
||||||
|
chapters Chapter[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chapter {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
mangaId Int
|
||||||
|
number Int
|
||||||
|
title String
|
||||||
|
pages Page[]
|
||||||
|
manga Manga @relation(fields: [mangaId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Page {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
chapterId Int
|
||||||
|
number Int
|
||||||
|
imageUrl String
|
||||||
|
chapter Chapter @relation(fields: [chapterId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
PUBLISHED
|
||||||
|
DRAFT
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core (build first)
|
||||||
|
- Manga listing page with cover images
|
||||||
|
- Manga detail page (title, description, chapter list)
|
||||||
|
- Chapter reader page (page-by-page image display)
|
||||||
|
- Internal search by manga title (PostgreSQL `contains` query)
|
||||||
|
|
||||||
|
### SEO
|
||||||
|
- Next.js SSR/SSG for all public pages
|
||||||
|
- `generateMetadata()` per page (title, description)
|
||||||
|
- Auto-generated `/sitemap.xml` via `app/sitemap.ts`
|
||||||
|
- Submit sitemap to Google Search Console
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- **Phase 1**: PostgreSQL `contains` with `insensitive` mode (sufficient for < 10k titles)
|
||||||
|
- **Phase 2** (future): Meilisearch for fuzzy/full-text search if needed
|
||||||
|
|
||||||
|
## URL Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/ Homepage (manga grid)
|
||||||
|
/manga/[slug] Manga detail + chapter list
|
||||||
|
/manga/[slug]/[chapter] Chapter reader
|
||||||
|
/api/search?q= Search API endpoint
|
||||||
|
/api/manga Manga CRUD (admin)
|
||||||
|
/api/upload R2 presigned URL generation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql://manga_user:yourpassword@localhost:5433/manga_db
|
||||||
|
|
||||||
|
R2_ACCOUNT_ID=your_cloudflare_account_id
|
||||||
|
R2_ACCESS_KEY=your_r2_access_key
|
||||||
|
R2_SECRET_KEY=your_r2_secret_key
|
||||||
|
R2_BUCKET=manga-images
|
||||||
|
R2_PUBLIC_URL=https://your-r2-public-url.r2.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nginx Reverse Proxy Config (aaPanel)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content Source
|
||||||
|
|
||||||
|
- **Public domain / free manga only** (no copyright issues)
|
||||||
|
- Sources: Comic Book Plus, Digital Comic Museum, Internet Archive
|
||||||
|
- Initial: manual download and upload
|
||||||
|
- Future: automated scraper (Node.js + cheerio) for allowed sources
|
||||||
|
|
||||||
|
## Recommended Build Order
|
||||||
|
|
||||||
|
1. Set up Prisma schema + connect to PostgreSQL
|
||||||
|
2. Build manga listing and reader pages (static UI first)
|
||||||
|
3. Wire up database queries via Prisma
|
||||||
|
4. Add R2 upload + image serving
|
||||||
|
5. Add search API
|
||||||
|
6. Add SEO metadata + sitemap
|
||||||
|
7. Dockerize and deploy to Proxmox via Gitea
|
||||||
|
|||||||
67
CLAUDE.md
67
CLAUDE.md
@ -1 +1,68 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
@AGENTS.md
|
@AGENTS.md
|
||||||
|
|
||||||
|
## Build & Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Start dev server (hot reload)
|
||||||
|
npm run build # Production build
|
||||||
|
npm start # Start production server
|
||||||
|
npm run lint # ESLint (flat config, v9+)
|
||||||
|
npx prisma generate # Regenerate Prisma client after schema changes
|
||||||
|
npx prisma migrate dev --name <name> # Create and apply a migration
|
||||||
|
npx prisma db push # Push schema changes without migration (dev only)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Stack**: Next.js 16.2.1 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · Prisma · PostgreSQL 16 · Cloudflare R2
|
||||||
|
|
||||||
|
**Path alias**: `@/*` maps to project root (configured in tsconfig.json).
|
||||||
|
|
||||||
|
### App Router Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── page.tsx # Homepage (manga grid)
|
||||||
|
├── layout.tsx # Root layout (Geist fonts, metadata)
|
||||||
|
├── manga/[slug]/page.tsx # Manga detail + chapter list
|
||||||
|
├── manga/[slug]/[chapter]/page.tsx # Chapter reader
|
||||||
|
├── api/search/route.ts # Search endpoint (?q=)
|
||||||
|
├── api/manga/route.ts # Manga CRUD (admin)
|
||||||
|
├── api/upload/route.ts # R2 presigned URL generation
|
||||||
|
└── sitemap.ts # Auto-generated sitemap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
- **Database**: Prisma ORM → PostgreSQL. Models: `Manga`, `Chapter`, `Page` (see `prisma/schema.prisma`).
|
||||||
|
- **Images**: Stored in Cloudflare R2 (S3-compatible). Upload flow: backend generates presigned URL → browser uploads directly to R2 → database records the image path.
|
||||||
|
- **R2 bucket layout**: `/manga/{manga_id}/{chapter_id}/{page_number}.webp` and `/thumbnails/{manga_id}/cover.webp`.
|
||||||
|
- **Search**: PostgreSQL `contains` with `insensitive` mode (phase 1). No external search engine needed until >10k titles.
|
||||||
|
|
||||||
|
### Key Libraries
|
||||||
|
|
||||||
|
- `@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner` — R2 uploads (S3-compatible API)
|
||||||
|
- `prisma` / `@prisma/client` — ORM and database client
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- **Docker**: `docker build` from root Dockerfile → container exposes port 3000, mapped to host port 3001.
|
||||||
|
- **Compose**: `docker-compose.yml` runs both the app and PostgreSQL 16.
|
||||||
|
- **Flow**: Push to Gitea → `git pull` on Proxmox Docker host → rebuild → restart via Portainer.
|
||||||
|
- **Reverse proxy**: aaPanel Nginx routes `manga.04080616.xyz` → `http://127.0.0.1:3001`.
|
||||||
|
- **Port 3001** (app) and **5433** (Postgres) on the host — many other ports are taken.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required in `.env` (see docker-compose.yml for container equivalents):
|
||||||
|
|
||||||
|
- `DATABASE_URL` — PostgreSQL connection string
|
||||||
|
- `R2_ACCOUNT_ID`, `R2_ACCESS_KEY`, `R2_SECRET_KEY`, `R2_BUCKET`, `R2_PUBLIC_URL` — Cloudflare R2
|
||||||
|
|
||||||
|
## Content Policy
|
||||||
|
|
||||||
|
All manga content must be **public domain or free** (Comic Book Plus, Digital Comic Museum, Internet Archive). No copyrighted material.
|
||||||
|
|||||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "start"]
|
||||||
63
app/page.tsx
63
app/page.tsx
@ -1,65 +1,14 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<div className="text-center">
|
||||||
<Image
|
<h1 className="text-4xl font-bold text-black dark:text-white">
|
||||||
className="dark:invert"
|
SunnyMH Manga
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
<p className="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
Hello World — Coming Soon
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
manga-app:
|
||||||
|
build: .
|
||||||
|
pull_policy: build
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://manga_user:${POSTGRES_PASSWORD}@manga-db:5432/manga_db
|
||||||
|
R2_ACCOUNT_ID: ${R2_ACCOUNT_ID}
|
||||||
|
R2_ACCESS_KEY: ${R2_ACCESS_KEY}
|
||||||
|
R2_SECRET_KEY: ${R2_SECRET_KEY}
|
||||||
|
R2_BUCKET: ${R2_BUCKET}
|
||||||
|
R2_PUBLIC_URL: ${R2_PUBLIC_URL}
|
||||||
|
depends_on:
|
||||||
|
- manga-db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
manga-db:
|
||||||
|
image: postgres:16
|
||||||
|
volumes:
|
||||||
|
- manga_pgdata:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: manga_user
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: manga_db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
manga_pgdata:
|
||||||
Loading…
x
Reference in New Issue
Block a user