Skip to main content

split-micro-app

Use for standalone micro apps: React Router SSR/SSG, Cloud overlays, Workers, CDN/R2, SEO/OG, gateway routes, previews, Docker integration and SSR bundle isolation.

Source facts

Repository
lobehub/lobehub
Last source activity
September 28, 2026 at 09:38
Detected SKILL.md language
English
Stars
83,263
Forks
15,990

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
split-micro-app
description
Use for standalone micro apps: React Router SSR/SSG, Cloud overlays, Workers, CDN/R2, SEO/OG, gateway routes, previews, Docker integration and SSR bundle isolation.
user-invocable
false
# Micro App: Split, SSR, SEO, Gateway Canonical living examples, both extracted 2026-08 — read the real files, this skill records the decisions and landmines, not copies of the code: - **`apps/workbench`** (`/verify`, `/acceptance`) — all code in this repo, builds and deploys from OSS CI. - **`apps/share`** (`/share/t/:id`, `/share/page/:id`) — renders Cloud-only surfaces, so it builds and deploys from **lobehub-cloud** CI. See §1b before touching it. - **`apps/auth`** (`/signin`, `/signup`, …) — the SSG variant: `ssr: false` + `prerender`, so the worker carries no React at all (7KB). 18 locales x 4 routes of prerendered documents. Also renders Cloud-only surfaces (§1b). See §3b. ## Hosting One app, two serve paths. Do not mix them. | Surface | Who serves it | Build | | ------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cloud | Gateway (torii) → Worker (SSR) | Cloud Next does **not** `build:spa:<name>`, copy `_spa-<name>`, rewrite to `/spa-<name>`, or CDN-upload that prefix | | OSS / 子部署 Docker | Next in the same image, client-rendered | `build:docker` runs `build:spa:<name>`; `<NAME>_REQUIRED=1` on `generateSpaTemplates`; Dockerfile copies `apps/<name>/package.json` before `pnpm i` and `public/_spa-<name>` into the runtime image | `generateSpaTemplates` skips a missing `dist/<name>` HTML unless `<NAME>_REQUIRED=1`. Cloud must skip. Docker must require. Do not revive Cloud `SPA_TARGET=<name>` Vite builds or a `/spa-<name>` middleware rewrite. The Docker chain per app, all five links or the route is dead: root `build:docker` script → `copySpaBuildCore.ts` target entry (`dist/<name>` → `public/_spa-<name>`) → `spaHtmlPaths.ts` resolver + `generateSpaTemplates` block → `src/app/spa-<name>/[locale]/[[...path]]/route.ts` → middleware rewrite (`src/libs/next/<name>Routes.ts` + `define-config.ts`) plus the path in `src/proxy.ts`'s matcher and, for public pages, in `isPublicRoute`. **Self-hosted loses per-page SSR.** The Next shell serves the built SPA HTML with a brand OG card and `noindex, nofollow`; per-subject title/OG only exists in the Worker build. That is the accepted trade, not a bug to chase. **Dev shells — a 200 that lies.** A `/spa-<name>` handler may only call `fetchViteDevTemplate('/index.<name>.html')` if that file exists at the repo **root** (workbench has one; it differs from `apps/<name>/index.html` only in the entry path, which must point at `/apps/<name>/src/entry.tsx`). Miss it and Vite's HTML fallback answers **200 with the main SPA shell** — the micro app never loads, nothing errors, and the main SPA no longer has those routes. An app developed against its own Vite server (share: `dev:spa:share`) carries **no dev branch at all**. `scripts/spaDevShells.test.ts` guards both directions. ## Crossing from the main SPA Only markdown internal entity links leave the main SPA (`InternalEntityLink` → `window.location.assign` when `shouldHardNavigateToWorkbench`). `Link` and `useWorkspaceAwareNavigate` stay in-router. Electron never hard-navs (portal). Do not add a `__WORKBENCH__` Vite define — the helper keys off the path (and skips Electron). Share needed none of this: the only producer builds an **absolute** URL for copy/open (`${appOrigin}/share/t/${id}` in `SharePopover`), which is already a hard navigation. Before deleting `src/routes/<x>/**`, grep for in-router links to those paths — a surviving `Link` lands on the main SPA's 404. Then drop the routes from `desktopRouter.config.tsx` / `mobileRouter.config.tsx` and update `desktopRouter.sync.test.tsx` + `routeScope.test.ts`. ## 1. Splitting & Artifacts App layout (`apps/<name>/`): ``` app/ # RR framework mode: root.tsx, routes.ts, entry.server.tsx, routes/, lib/, stubs/ workers/app.ts # worker entry: API reverse proxy + createRequestHandler src/ # app-owned features/shell (may coexist with a legacy SPA entry) vite.config.rr.mts # RR pipeline; legacy vite.config.ts can coexist (RR CLI: -c vite.config.rr.mts) wrangler.jsonc # name, account_id, nodejs_compat, vars (API base / app home) staticCssOptions.mjs # ONE source for static-css hrefTemplate (vite plugin + emit + dev middleware) ``` Reuse main-src code via `@/*` deep imports + Vite 8 native `resolve.tsconfigPaths: true` (the vite-tsconfig-paths **plugin** breaks dev SSR module-runner resolution; the app's tsconfig `include` must cover `app/`). Stack: `@react-router/dev@8` + `@cloudflare/vite-plugin` - repo Vite 8 (rolldown) — officially compatible. **SSR bundle weight is the whole battle.** Main-src imports drag the app universe (store web → chat/agent/electron). Tools and cuts, in order: | Tool / cut | How | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Trace who pulls a module | `<APP>_TRACE_MODULE=store/chat/store [<APP>_TRACE_ENV=client] bun run build:rr` — prints importer chain (`SHARE_`/`WORKBENCH_`). In an overlay repo, run it there too (§1b) | | Client-gate heavy routes | `.client.tsx` module + `clientOnlyRoute()` factory (hydration gate; SSR renders loading) | | SSR-stub store hubs | vite `resolveId` stubs per env (`app/stubs/`): trpc client, services/global, store/electron, store/file, store/user, i18n loader. Stubs must be **callable empty-state hooks**, not throwing proxies — render paths call them. Keep the implemented surface explicit (no catch-all `Proxy` / `as never`): `stubSurfaceGuard` in `vite.config.rr.mts` fails the build when the graph imports an export or member the stub does not implement. Add the member (empty / `reject`) or keep the importer off the graph. | | Client-shim lambda | Client `@/libs/trpc/client` is a cookie-only `httpLink` (`trpcClient.client.ts`). Do not ship the real `lambda.ts` — its `headers()` pulls image store → chat store + `model-bank` catalog. | | SSR-stub shiki langs | On the SSR env, resolve `@shikijs/langs` / `@shikijs/themes` / shiki's `langs-bundle` / `themes` / wasm to `app/stubs/shiki.ts`. Do **not** stub the `shiki` package entry — Pierre diffs and Highlighter import named APIs from it. | | Client shiki from CDN | On the client env, resolve `shiki` / `shiki/*` / `@shikijs/*` to pinned `https://esm.sh/...@<installed shiki version>` as externals. Do not bundle grammars or wasm into `build/client`. | | Slot-inject app-only features | Context seam owned by the shared feature (see `src/features/Acceptance/Viewer/Conversation/originConversation.tsx`); app provides, micro app leaves null → affordances hide | | Bypass barrels | Deep-path imports (`@/features/Acceptance/Acceptance`, not the barrel) — barrels evaluate sibling exports with side effects | | Compose capability atoms | Shared UI is assembled per surface; the light app never imports the fat viewer. See **`compose-atoms`**. Do not add `readOnly` on the in-app page | | Decouple dual-use components | Lift store reads to optional props (see AudioPlayer `uploadState`/`onCancelUpload`) | Share's cuts took SSR from **9.84MB → 1.78MB gzip**. Two build-level snags worth knowing: `resolve.dedupe` must include `@lobehub/ui` (the `builtin-tool-*` packages declare a loose `^5` and resolve to an older copy whose base-ui lacks components the app renders → `MISSING_EXPORT: Alert`), and a `*.client` module needs its own SSR-env stub so the hydration gate does not drag the gated tree into the worker anyway. Products: `build/client` (assets → CDN), `build/server` (worker; deploy with `wrangler deploy --config build/server/wrangler.json`). Budget: worker gzip ≤ 10MB paid. **CI affected-detection**: build emits `build-inputs.txt` (module-graph file list, gitignored); the deploy workflow diffs changed files against the manifest from the **last successful run's artifact** (carry-forward on skip), plus meta triggers (app dir, plugins/vite, lockfile, tsconfig, glob dirs). New `import.meta.glob` patterns in shared code need a new meta trigger — the one manual rule. See `apps/workbench/scripts/should-build.mjs` + `.github/workflows/deploy-workbench.yml`; the overlay-hosted variant is lobehub-cloud's `scripts/shouldBuildShare.ts` + `.github/workflows/deploy-share.yml` (§1b). The manifest is only a pre-filter: it lists every module the build loaded, so type-only files, barrels and tree-shaken code trigger builds too. The deploy decision is the **output hash** (`scripts/workerOutputHash.ts`): the build is deterministic (comment-only edits hash identically), so `deploy.ts` hashes `build/`, compares it with the `output-hash.txt` carried in the same artifact, and skips upload + `wrangler deploy` when equal — keeping Cloudflare's 100-version rollback window for real releases. Manual dispatch passes no previous hash and always deploys. Workbench has it; share does not yet. **PR-time verify is a separate workflow per repo that can change the artifact** — deploy ownership (§1b) does not decide verify ownership. Each verify builds the worker, uploads a non-deployed preview **version** (`wrangler versions upload --preview-alias`, dry-run when secrets are absent), enforces the 8MB-gzip guard, and comments the preview URL behind an HTML marker (never `--edit-last` — other workflows comment as the same bot). Workbench: one repo, one `verify-workbench.yml`. Share: **both** repos, because either side's change flows into the
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub