Skip to content

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ẫnNộ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/:idshowcase dự án thật (NEMO, ArtLove, HOATIEU), dữ liệu ở packages/media-catalog/showcases.ts
/services/:idtrang 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/pricingbảng giá, chỉ mở khi đã đăng nhập VÀ có hồ sơ (RequireProfile)
/services/:id/requestform nhận báo giá; ?trial=1 bật dùng thử, ?tier=&<nhóm>=&addons=a,b giữ lựa chọn
/quote/:idkế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:

FileNội dung
services.ts, quote.ts, prices.tsxem mục "Giá nằm ở đâu" bên dưới
cases.tsCASES (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 ​

  1. Form gửi POST /api/media/quotes → dòng media_quote_requests với status='draft'.
  2. Trình duyệt sang GET /api/media/quotes/:id/google → ghi member_oauth_states với next_path='media-quote:<id>' → Google.
  3. Google gọi về callback CHUNG /api/user/auth/google/callback (Google chỉ khai một redirect_uri). Callback thấy tiền tố media-quote: thì rẽ sang finishMediaQuote() (domains/media/index.ts): chỉ xác minh email, không tạo tài khoản users hay phiên thành viên.
  4. 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ồi sendEmail({ 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.
  5. 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/callbackstate 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 /logoutphiên và hồ sơ doanh nghiệp
GET /pricingbả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 /quotes403 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:

FileAi importNội dung
services.tsapi + giao diệncấu trúc dịch vụ, gói, tuỳ chọn, add-on. Không có số tiền
quote.tsapi + giao diệnhàm quote(), kiểu PricedService, vnd(). Không có số tiền
prices.tschỉ apiPRICES 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ánh media-login: của callback.
  • test:media-gate gọi thẳng app con /api/media và finishMediaLogin(), chưa chạy nhánh media-login: trong callback chung lẫn middleware của src/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ũ (/me trả 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á.

Tài liệu nội bộ nền tảng Conan School.