Thành phần | Công nghệ đề xuất |
---|---|
Front-end | Next.js (React) với SSR/SSG |
API backend | Node.js + Express/Koa, REST hoặc GraphQL |
Database | PostgreSQL hoặc MongoDB |
Cache | Redis hoặc Memcached |
Lưu trữ tĩnh | AWS S3 + CloudFront CDN |
Loại Dữ Liệu | Công Nghệ Gợi Ý | Mục Đích |
---|---|---|
Metadata trò chơi | PostgreSQL hoặc MongoDB | Lưu trữ thông tin trò chơi |
Dữ liệu truy cập nóng | Redis | Cache dữ liệu thường xuyên dùng |
// pages/category/[slug].jsimport { getGamesByCategory } from '../../lib/api'
export async function getStaticPaths() { const categories = await fetchCategories() return { paths: categories.map(cat => ({ params: { slug: cat.slug } })), fallback: false }}
export async function getStaticProps({ params }) { const games = await getGamesByCategory(params.slug) return { props: { games } }}
export default function CategoryPage({ games }) { return ( <div> <h1 className="text-3xl font-bold capitalize">{games[0]?.category}</h1> <ul className="grid grid-cols-2 md:grid-cols-4 gap-4"> {games.map(game => ( <li key={game.id}> <GameCard game={game} /> </li> ))} </ul> </div> )}
import Image from 'next/image'import { useInView } from 'react-intersection-observer'
export default function LazyImage({ src, alt }) { const [ref, inView] = useInView({ triggerOnce: true }) return ( <div ref={ref} className="relative w-full h-48 bg-gray-200"> {inView && ( <Image src={src} alt={alt} layout="fill" objectFit="cover" placeholder="blur" blurDataURL="/placeholder.png" /> )} </div> )}
Cột | Kiểu Dữ Liệu | Mô Tả |
---|---|---|
id | SERIAL (PK) | Mã định danh game |
title | VARCHAR(255) | Tiêu đề trò chơi |
slug | TEXT (unique) | Đường dẫn thân thiện URL |
category | VARCHAR(50) | Danh mục |
description | TEXT | Mô tả trò chơi |
thumbnail_url | TEXT | URL hình đại diện |
embed_url | TEXT | URL nhúng trò chơi |
created_at | TIMESTAMP | Ngày tạo |
CREATE TABLE games ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, slug TEXT NOT NULL UNIQUE, category VARCHAR(50) NOT NULL, description TEXT, thumbnail_url TEXT, embed_url TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW());
ZSET
.import redis from '../lib/redis'import db from '../lib/db'
async function updateTrending() { const topGames = await redis.zrevrange('game:plays', 0, 19) await db.query('UPDATE categories SET trending = $1 WHERE name = $2', [topGames, 'all'])}
<Image>
của Next.js tự động tối ưu hóa hình ảnh, kết hợp CDN lưu trữ.Loại Tài Nguyên | Headers Cache-Control |
---|---|
Tài nguyên tĩnh | public, max-age=31536000, immutable |
Trang HTML | public, max-age=60 |
<head>
tùy theo nội dung trò chơi:import Head from 'next/head'
export default function GamePage({ game }) { return ( <> <Head> <title>{game.title} – Free Hypercasual Play</title> <meta name="description" content={game.description || 'Play free hypercasual games online'} /> <link rel="canonical" href={`https://risequestgame.top/${game.slug}`} /> </Head> {/* Nội dung chính trang */} </> )}
game_played
, category_view
, ad_click
.name: CI/CD
on: [push]
jobs: build-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Deploy Frontend uses: amondnet/vercel-action@v20 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}