media.conan.school
Trang dịch vụ của Conan Media: content creation, message architecture, customer research, SEO, social media content, video creation. Vite + React + Tailwind v4 (token chung) + shadcn/ui + Motion. Deploy qua deploy-media.yml vào Pages project media-conan-school (production branch main), domain media.conan.school gắn vào project ngày 08.10.2026.
Trang
| Đường dẫn | Nội dung |
|---|---|
/ | hero chữ lớn, dải tên chạy, dự án tiêu biểu, 8 dịch vụ, quy trình. Không có giá |
/work/:id | showcase dự án thật (NEMO, ArtLove, HOATIEU), dữ liệu ở packages/media-catalog/showcases.ts |
/services/:id | trang riêng của dịch vụ: ca minh hoạ, gói dùng thử, dự án liên quan. Không có giá |
/login, /onboarding | đăng nhập Google, rồi khai hồ sơ doanh nghiệp sơ bộ |
/pricing, /services/:id/pricing | bảng giá, chỉ mở khi đã đăng nhập VÀ có hồ sơ (RequireProfile) |
/services/:id/request | form nhận báo giá; ?trial=1 bật dùng thử, ?tier=&<nhóm>=&addons=a,b giữ lựa chọn |
/quote/:id | kết quả sau khi đăng nhập Google |
URL tiếng Anh từ 09.10.2026; đường cũ /dich-vu/..., /bao-gia/... (và ?thu, ?goi, ?them) tự chuyển sang tên mới vì thư báo giá đã gửi còn mang link cũ. Link cũ /#seo (bản đầu mở dịch vụ trong hộp thoại) tự chuyển sang /services/seo. SPA nên Pages cần public/_redirects (/* /index.html 200), thiếu file đó là mọi trang con trả 404.
Bảng giá và ca thành công nằm ở đâu
packages/media-catalog/, dùng chung cho giao diện VÀ api:
| File | Nội dung |
|---|---|
services.ts, quote.ts, prices.ts | xem mục "Giá nằm ở đâu" bên dưới |
cases.ts | CASES (3 ca thành công mỗi dịch vụ) và TRIALS (gói dùng thử) |
quote() là chỗ duy nhất tính giá: giá gói, rồi từng nhóm tuỳ chọn theo thứ tự khai báo ((giá + delta) * multiplier), rồi add-on. Đổi thứ tự nhóm là đổi giá.
Giá tính lại ở máy chủ. POST /api/media/quotes nhận lựa chọn chứ không nhận con số, rồi gọi cùng quote(). Vì vậy sửa packages/media-catalog/ phải deploy CẢ api lẫn media (hai workflow đều khai đường dẫn này); deploy lệch là trang hiện một giá, thư báo giá ghi giá khác.
Ca thành công là hư cấu (người soạn yêu cầu 09.10.2026), tên doanh nghiệp cố ý không trùng thương hiệu có thật. Luật story-world vẫn áp: không bao giờ gán số liệu bịa cho thương hiệu thật. Có ca thật được khách đồng ý công bố thì thay vào.
Luồng báo giá qua Gmail
- Form gửi
POST /api/media/quotes→ dòngmedia_quote_requestsvớistatus='draft'. - Trình duyệt sang
GET /api/media/quotes/:id/google→ ghimember_oauth_statesvớinext_path='media-quote:<id>'→ Google. - Google gọi về callback CHUNG
/api/user/auth/google/callback(Google chỉ khai mộtredirect_uri). Callback thấy tiền tốmedia-quote:thì rẽ sangfinishMediaQuote()(domains/media/index.ts): chỉ xác minh email, không tạo tài khoảnusershay phiên thành viên. UPDATE ... SET status='sent' WHERE status='draft'giành quyền gửi (hai callback chồng nhau chỉ một lượt đổi được dòng), rồisendEmail({ kind: 'media_quote' })gửi tới email Google đã xác minh, qua Cloudflare Email Service như mọi thư khác. "Gửi qua Gmail" nghĩa là gửi TỚI hộp Gmail của khách.- Chuyển về
/quote/:id.
Thư chỉ đi tới email đã xác minh, nên form công khai không dùng để gửi thư rác cho người khác được.
Giá chỉ mở sau đăng nhập (12.10.2026)
Người soạn quyết định: trang chủ và trang dịch vụ không hiện giá; khách đăng nhập Google, khai hồ sơ doanh nghiệp, rồi mới thấy bảng giá. Ranh giới nằm ở API, không ở giao diện.
Endpoint (/api/media) | Việc |
|---|---|
GET /auth/google?next= | sang Google; next chỉ nhận đường dẫn nội bộ, lạ thì về /pricing |
callback chung /api/user/auth/google/callback | state tiền tố media-login: → finishMediaLogin(): upsert media_clients theo google_subject, tạo media_sessions, cookie conan_media_session (HttpOnly, Lax, path /api/media, 30 ngày) |
GET /me, PUT /me/profile, POST /logout | phiên và hồ sơ doanh nghiệp |
GET /pricing | bảng giá; 401 chưa đăng nhập, 403 chưa khai hồ sơ. Route tự đặt private, no-store; từ 17.10.2026 middleware chung giữ header route tự đặt cho GET 200 (shared/cache.ts), nên production thấy đúng private, no-store (vẫn không cache) |
POST /quotes | 403 nếu chưa có hồ sơ; khách đã xác minh email lúc đăng nhập nên thư gửi ngay, không đăng nhập lần hai. Tối đa 5 yêu cầu mỗi khách mỗi giờ (429), điều kiện nằm trong chính câu INSERT … SELECT … WHERE để hai lượt chồng nhau không cùng lọt |
Khách doanh nghiệp không phải thành viên: bảng riêng (media_clients, migration 20261112a_media_clients.sql), không đụng users/legacy_users.
Giá nằm ở đâu
packages/media-catalog/ tách làm ba:
| File | Ai import | Nội dung |
|---|---|---|
services.ts | api + giao diện | cấu trúc dịch vụ, gói, tuỳ chọn, add-on. Không có số tiền |
quote.ts | api + giao diện | hàm quote(), kiểu PricedService, vnd(). Không có số tiền |
prices.ts | chỉ api | PRICES và pricedServices(); thiếu giá một gói là ném lỗi lúc khởi động |
Gác: media.conan.school/scripts/check-no-prices.mjs chạy cuối npm run build, đọc mọi giá trong prices.ts rồi tìm trong dist/ (cả dạng 45e6 của bản minify). Bỏ qua luỹ thừa của 10 vì Motion có hằng số 1e7. Phép kiểm API: npm run test:media-gate (job api của CI, D1 giả bằng node:sqlite).
Xem thử ở máy
npm run dev:bypass: nút Google đăng nhập ngay bằng tài khoản giả, hồ sơ lưu localStorage, giá nạp động từ prices.ts, báo giá không gửi lên api (/quote/demo). Cờ chỉ có hiệu lực khi import.meta.env.DEV, nên nhánh đó bị loại khỏi bản build, và gác ở trên kiểm lại điều đó.
/quote/:id không được phụ thuộc phiên
Trang này là đích của nút "Xem lại báo giá" trong mọi thư (kể cả thư cũ qua /bao-gia/:id), và người mở thường không có phiên: điện thoại khác, trình duyệt khác, thư gửi trước khi có đăng nhập. Tên dịch vụ lấy từ danh mục công khai, giá và số ngày lấy từ chính yêu cầu báo giá (GET /quotes/:id); bảng giá đầy đủ chỉ hiện thêm khi có phiên. Bản đầu của PR #419 đọc /pricing nên với người không đăng nhập trang quay "Đang tải…" mãi; lượt rà trước merge bắt được (13.10.2026).
Showcase
Showcase là thương hiệu THẬT: không bịa số liệu; results để rỗng tới khi có số thật.
Còn mở
- Chưa có màn quản trị cho
media_quote_requests; người vận hành thấy yêu cầu qua bản sao kiểm tra của thư. - SPA chưa SSR: nội dung từng dịch vụ chỉ nằm trong JS.
- Trạng thái OAuth chưa gắn với trình duyệt đã bắt đầu luồng (login CSRF): ai có URL callback còn hạn 10 phút của người khác có thể cài phiên của họ vào trình duyệt nạn nhân. Sửa: cookie nonce ngắn hạn khi
GET /auth/google, so ở nhánhmedia-login:của callback. test:media-gategọi thẳng app con/api/mediavàfinishMediaLogin(), chưa chạy nhánhmedia-login:trong callback chung lẫn middleware củasrc/index.ts.- deploy-api và deploy-media chạy song song khi merge: khoảng một phút SPA mới gọi API cũ (
/metrả 404, trông như chưa đăng nhập). Theo dõi deploy-api xanh ngay sau merge.
Bẫy đã xảy ra: wrangler pages project create của wrangler 4
Lệnh này (08.10.2026) KHÔNG tạo Pages project: nó tạo một Worker rỗng cùng tên, rồi tự sửa thư mục site (thêm wrangler.jsonc, @cloudflare/vite-plugin vào vite.config.ts, đổi script deploy sang wrangler deploy). Những thay đổi đó lọt vào PR đầu mà không ai thấy, vì build vẫn xanh. Đã gỡ ở PR báo giá. Tạo Pages project thì gọi API POST /accounts/:id/pages/projects. Worker rỗng media-conan-school vẫn còn trong tài khoản, vô hại, chờ người vận hành xoá.