目標:完成互動式個人網站資訊架構(IA)、導覽設計、版型草圖、資料模型;規劃留言板 / 註冊登入 Demo 的後端 API。
/
、/about
、/works
、/posts
、/contact
Navbar
、Hero
、WorkCard
、Footer
、ContactForm
、Toast
資料模型
{ id, username, passwordHash, createdAt }
{ id, name, message, createdAt }
路由表
POST /api/auth/register
→ 註冊(Demo:不啟用 Email 驗證)POST /api/auth/login
→ 登入(回傳 JWT 或 demo token)GET /api/comments
→ 取得留言列表POST /api/comments
→ 新增留言需求細節
檔案結構
my-portfolio/
├─ client/ # 前端 (Vite/React 或原生 HTML+CSS+JS)
└─ server/ # 後端 (Express)
├─ db/ # SQLite / JSON 資料檔
├─ .env # PORT, JWT_SECRET
├─ package.json
└─ index.js
下面提供最小可行後端程式碼(可選 JSON 檔案或 SQLite):
server/index.js
(精簡版)
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const app = express();
app.use(cors());
app.use(express.json());
const dataPath = path.resolve('server/db');
const usersFile = path.join(dataPath, 'users.json');
const commentsFile = path.join(dataPath, 'comments.json');
if (!fs.existsSync(dataPath)) fs.mkdirSync(dataPath, { recursive: true });
if (!fs.existsSync(usersFile)) fs.writeFileSync(usersFile, '[]');
if (!fs.existsSync(commentsFile)) fs.writeFileSync(commentsFile, '[]');
const read = f => JSON.parse(fs.readFileSync(f, 'utf-8'));
const write = (f, data) => fs.writeFileSync(f, JSON.stringify(data, null, 2));
const hash = pw => crypto.createHash('sha256').update(pw).digest('hex');
app.post('/api/auth/register', (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ error: '缺少欄位' });
const users = read(usersFile);
if (users.find(u => u.username === username)) return res.status(409).json({ error: '帳號已存在' });
const user = { id: crypto.randomUUID(), username, passwordHash: hash(password), createdAt: Date.now() };
users.push(user); write(usersFile, users);
res.json({ ok: true });
});
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
const users = read(usersFile);
const ok = users.find(u => u.username === username && u.passwordHash === hash(password));
if (!ok) return res.status(401).json({ error: '帳密錯誤' });
// Demo token(僅示範)
res.json({ token: 'demo-' + crypto.randomBytes(6).toString('hex') });
});
app.get('/api/comments', (req, res) => {
res.json(read(commentsFile));
});
app.post('/api/comments', (req, res) => {
const { name, message } = req.body;
if (!name || !message) return res.status(400).json({ error: '缺少欄位' });
const list = read(commentsFile);
const c = { id: crypto.randomUUID(), name, message, createdAt: Date.now() };
list.push(c); write(commentsFile, list);
res.json(c);
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log('API on http://localhost:' + PORT));
啟動
cd server
npm init -y
npm i express cors
node index.js
better-sqlite3
users(id, username, passwordHash, createdAt)
、comments(id, name, message, createdAt)