---
url: https://docs.nemo12.com/ops/learn-split.md
description: >-
  Mỗi program một app và một Worker trên learn.nemo12.com theo route path; chung
  đăng nhập, API, D1, package; docs cạnh code (SRC-1432).
---

# Tách learn thành web app riêng cho từng program

Chủ dự án 18.10.2026 chọn "Cách 3" trong ba phương án: mỗi program là một web app riêng, **cùng repo, cùng tên
miền** `learn.nemo12.com`, chung đăng nhập, API (`workers/api`), D1 và các package. Mục tiêu: sửa một program chỉ
test và deploy program ấy, lỗi một program không làm hỏng program khác, và mỗi phiên Claude làm trong một thư mục
riêng. Cách 2 (subdomain và repo riêng) bị loại vì ba program chung tài khoản, dữ liệu và code; tách repo chỉ hợp
với phần không chung dữ liệu hay đăng nhập (như nemo12-www, nemo12-videos). Tên miền giữ nguyên theo chỉ đạo
18.09.2026 (mọi URL trên learn.nemo12.com, cổng `check-one-domain`).

## Cách định tuyến

Route của Cloudflare thắng custom domain trên cùng hostname, và route cụ thể hơn thắng route chung. Nên:

* `nemo12-learn` (shell) giữ custom domain `learn.nemo12.com`: trang chủ, `/programs`, hồ sơ, chứng chỉ, lịch,
  các redirect 301 (`/math`, `/ai-teen`, `/speaking/*`) và bản catch-all.
* Mỗi program một Worker với hai route, ví dụ `learn.nemo12.com/grammar` và `learn.nemo12.com/grammar/*`.
* Mỗi app build với `base: "/<program>/"`, nên asset của nó nằm dưới tiền tố riêng, không đụng `/assets/*` của app khác.
* Cookie `nemo12_session` gắn `Domain=.nemo12.com; Path=/`, đăng nhập Google là nút trong trang (GIS), không có
  redirect URI: tách theo path giữ nguyên phiên đăng nhập và localStorage.
* Link sang app khác phải là tải lại trang (full page load), không `pushState`: router của app này không biết
  trang của app kia.
* Rollback: gỡ route của program là trang lại do `nemo12-learn` phục vụ như cũ.

## Các bước

| Bước | Việc | Trạng thái |
| --- | --- | --- |
| 0 | Gom shell dùng chung vào `packages/learn-shell`: đăng nhập, HTTP, NavBar, menu, avatar, Motion, ngôn ngữ, màn `Join`, `NemoLogo`, `PayCard` | Xong 18.10.2026 |
| 1 | Pilot GRAMMAR: `apps/grammar`, Worker `nemo12-grammar`, docs cạnh code, khu `design/` | Xong 18.10.2026, đang chạy trên production |
| 2a | SAT: `apps/sat`, Worker `nemo12-sat` | Xong 18.10.2026 |
| 2b | ESSAY: `apps/essay`, Worker `nemo12-essay` | Xong 18.10.2026 |
| 2c | STYLE (`/style`) | Giữ trong learn: đó là bảng mẫu giao diện của chính learn (27 component), không phải chương trình học; tách ra phải chép cả bộ component mà không được gì |
| 2d | WALK | Chưa: một phiên khác đang sửa các trang walk; tách khi khu ấy rảnh |
| 3 | IELTS cùng SPEAK (chung router, shell và auth nên tách cùng nhau, làm sau cùng) | Chưa |

Bước 0 không đổi hành vi: file chuyển nguyên văn, import trong `apps/learn` trỏ sang package. Primitive shadcn
không chép vào package: component trong shell import `@/components/ui/...`, phân giải về bản sao của chính app
dùng nó (cổng `check-ds-copies` giữ các bản sao giống hệt nhau); app nào dùng shell phải có các primitive ấy và
khai `@source` của package trong CSS để Tailwind quét class.

## Docs và artifact cạnh code

Chủ dự án 18.10.2026 chọn đặt tài liệu của mỗi web app cạnh code. Mỗi app có:

* `apps/<app>/docs/`: PRD, SDD, runbook, quyết định UI, ảnh trước/sau. Docs dùng chung (platform, API, D1) ở `docs/`.
* `apps/<app>/design/prototypes/`: prototype UI (nháp HTML/React, link Artifact, link Figma), không vào build.
* `apps/<app>/design/assets.json`: danh mục file nguồn và media trên R2 (khoá, nguồn, giấy phép, ghi công).
* `apps/<app>/CLAUDE.md`: luật riêng của app, Claude Code nạp khi làm trong thư mục ấy.

Nơi lưu từng dạng file: [quy tắc lưu trữ](storage.md).

## Phiên Claude theo làn

Mỗi web app một làn: phiên của làn ấy chỉ sửa `apps/<app>/`. Làn `platform` (một phiên một lúc) sửa
`workers/api`, `migrations/`, `packages/*` và shell. Việc cần cả API lẫn giao diện: làn platform làm API và
migration (expand/contract) trước, push lên `main`, rồi làn app làm giao diện. Làm thẳng trên `main`
([hệ thống test và QC](qc-system.md)).

## Rủi ro đã biết

1. Hai bộ đăng nhập và hai API client song song trong learn (bộ A: `me` trong `App.tsx`, `LaunchNotice`, `Join`;
   bộ B: `ielts/auth`). App tách ra phải tái tạo đúng cổng đăng nhập; E2E của program là cổng cho việc này.
2. Import chéo giữa program (grammar và walk dùng `ielts/`, speak dùng `walk/`, essay dùng `ielts/PayCard`) phải hết
   trước khi tách program ấy.
3. Một bộ mock E2E chung (`e2e/fixtures.ts`) cho 53 spec; mỗi app tách ra cần phần mock của mình.
