NCUFresh 多年份架構全貌

從完成後的樣子出發,說明整個專案的運作方式、每年要做什麼、以及幾個關鍵技術問題的解答。

做完之後,整個系統長這樣

一個 repo、一個 domain,同時跑多個年份的新生網站。使用者看到的是同一個網站,Nginx 根據 URL 路徑把請求分給對應年份的前端。

🌐 使用者瀏覽器
ncufresh.ncu.edu.tw
Nginx(反向代理)
根據 URL 路徑分流
路徑對應
/ → 2026 前端 Container: frontend-2026
/25/ → 2025 前端 Container: frontend-2025
/24/ → 2024 前端 Container: frontend-2024
統一後端(Express) Container: backend
MariaDB多個 database(ncufresh24, ncufresh25, ncufresh26)
RedisSession / Cache

重點:前端每年一個獨立 container,因為每年的 Nuxt 版本、設計、頁面結構都不同。但後端只有一個 container,用 route prefix 區分年份。資料庫也只有一個 MariaDB container,裡面開不同的 database。


Repo 的目錄結構

整個 monorepo 的最終樣子:

ncufresh/ ├── base/ ← 共用的 Nuxt Layer │ ├── composables/ │ │ ├── useAuth.ts ← 登入狀態管理 │ │ └── useApi.ts ← API fetch wrapper │ ├── middleware/ │ │ └── auth.ts │ └── nuxt.config.ts │ ├── sites/ ← 每年一個資料夾 │ ├── 2026/ ← 當年度(最新) │ │ ├── pages/ │ │ ├── components/ │ │ ├── nuxt.config.ts ← extends: ['../../base'] │ │ ├── package.json │ │ └── Dockerfile │ ├── 2025/ ← 去年 │ │ ├── pages/ │ │ ├── nuxt.config.ts ← baseURL: '/25/' │ │ ├── package.json │ │ └── Dockerfile │ └── 2027/ ← 明年(到時候再加) │ ├── backend/ ← 統一後端 │ ├── routes/ │ │ ├── auth.route.js │ │ ├── blog.route.js ← 26 的 routes │ │ ├── 25/ ← 25 的 routes,掛在 /api/25/* │ │ └── 24/ │ └── Dockerfile │ ├── nginx/ │ └── nginx.conf ├── docker-compose.yml ├── init-db.sql └── .gitlab-ci.yml

關鍵問題解答

Docker 會同時跑好幾個 container 嗎?不能合併?
前端:每年一個 container,不合併。因為每年的 Nuxt 版本不同(25 是 Nuxt 3、26 是 Nuxt 4)、npm 依賴不同、build 出來的東西完全不同,硬合在一起只會製造問題。

後端:只有一個 container。所有年份的 API route 都跑在同一個 Express app 裡,用路徑前綴(/api/25//api/26/)區分。

資料庫:只有一個 MariaDB container。裡面開多個 database(ncufresh25ncufresh26),資料完全隔離,但共用同一個 DB server。

實際跑起來的 container 列表:
docker compose ps
frontend-2026Nuxt 4 · port 3000
frontend-2025Nuxt 3 · port 3000
frontend-2024Nuxt 3 · port 3000
backendExpress · 統一處理所有年份的 API
MariaDB內含 ncufresh24, ncufresh25, ncufresh26 三個 DB
RedisSession & Cache
Nginx反向代理 · 路徑分流
每個前端 container 內部都監聽 port 3000,但它們是獨立的 Docker container,彼此不衝突。Nginx 根據 URL 路徑決定把請求送到哪個 container。
前端都在 ncufresh.ncu.edu.tw 底下,localStorage 和 Pinia 會衝突嗎?
短答:Pinia 不會衝突,localStorage 會,但加上 namespace 就解決了。

先搞清楚一件事:雖然 2025 和 2026 的前端跑在不同路徑(/25/ vs /),但它們的 domain 都是 ncufresh.ncu.edu.tw。在瀏覽器的眼中,這是同一個 origin
瀏覽器儲存機制與衝突風險
localStorage — ⚠️ 共用,會衝突 同一個 origin 下所有路徑共享同一個 localStorage。如果 2025 和 2026 都存了一個叫 token 的 key,它們會互相覆蓋。
sessionStorage — ⚠️ 共用(同分頁時) 同理,同一個瀏覽器分頁裡如果從 /25/ 導航到 / 也會共享。
Cookies — ⚠️ 預設共用 Cookie 預設 path 是 /,所有年份都看得到。可以用 path 屬性限縮。
Pinia(記憶體狀態)— ✅ 不衝突 每個年份是獨立的 Nuxt app,Pinia store 活在各自的 JavaScript 記憶體裡,互不干擾。
解法:加年份前綴(namespace)。把所有寫入 localStorage 的 key 加上年份標記,避免衝突。
// ❌ 會衝突 — 2025 和 2026 都寫同一個 key localStorage.setItem('token', jwtToken) localStorage.setItem('user', JSON.stringify(userData)) // ✅ 加年份前綴,完全隔離 const YEAR = '2026' localStorage.setItem(`ncufresh_${YEAR}_token`, jwtToken) localStorage.setItem(`ncufresh_${YEAR}_user`, JSON.stringify(userData))
特別注意 pinia-plugin-persistedstate — 如果你有用這個套件把 Pinia store 持久化到 localStorage,它預設的 key 是 store 的 id(例如 user)。兩個年份的 store 如果 id 一樣就會互蓋。解法是在 persist 設定裡自訂 key:
// sites/2026/stores/user.ts export const useUserStore = defineStore('user', { state: () => ({ name: '', token: '' }), persist: { key: 'ncufresh-2026-user', // ← 自訂 key,不會和其他年份衝突 }, })

新的一年要做什麼?

假設現在是 2027 年,要開新一屆的新生網站。以下是完整步驟。

把上一年的前端「降級」到子路徑

2026 目前跑在根路徑 /。加一行設定把它搬到 /26/,讓出根路徑給 2027。修改 sites/2026/nuxt.config.ts,加上 app: { baseURL: '/26/' }。同時更新 Nginx 設定,加一個 location /26/ 的 block。

建立 2027 的前端

sites/ 下建立 2027/ 資料夾。可以從 2026 複製骨架,或從零開始。重點是 nuxt.config.ts 裡要 extends: ['../../base'] 來繼承共用的 composables 和 middleware。根路徑 / 留給它。

後端加上 2027 的 routes

backend/routes/ 下建立 27/ 資料夾,寫新的 route handlers。掛到 app.use('/api/27/', ...)。如果業務邏輯沒變,可以直接複用 26 的 handler 只改 DB 連線。

開一個新的 Database

init-db.sql 加一行 CREATE DATABASE IF NOT EXISTS ncufresh27;。跑 migration 建表。

更新 docker-compose.yml

加一個 frontend-2027 service,指向 sites/2027/Dockerfile。Nginx 設定的 location / 改為 proxy 到 frontend-2027

更新 CI/CD

.gitlab-ci.ymlfrontend-2027-build job,用 changes 條件限定只在 sites/2027/**/*base/**/* 改動時觸發。

每年重複的工作量其實不大——核心就是:建資料夾、寫設定、加 routes、開 DB、更新 docker-compose。基礎設施(Nginx、CI、base layer)第一次建好後就不太需要動了。

一張圖看完整個架構邏輯

前端 — 各年獨立

每年一個 Nuxt app、一個 Docker container、一個子路徑。最新年度佔根路徑 /,舊年份依序退到 /26//25//24/。它們共享 base/ layer 裡的 composables 和 middleware,但頁面、元件、樣式完全獨立。

後端 — 統一一個

一個 Express app 處理所有年份的 API。用 route prefix 區分(/auth/ 給最新年度,/api/25//api/24/ 給舊年份)。OAuth callback 透過 state 參數判斷年份,導回正確的前端路徑。

資料庫 — 一個 server,多個 database

同一個 MariaDB instance 裡開多個 database(ncufresh24ncufresh25ncufresh26),資料完全隔離。不需要跑多個 DB container。

瀏覽器儲存 — 需要 namespace

因為所有年份都在同一個 domain 下,localStorage 是共享的。所有寫入 localStorage 的 key 都要加年份前綴(如 ncufresh_2026_token),Pinia 的 persistedstate 也要自訂 key。記憶體中的 Pinia state 不會衝突。