본문으로 건너뛰기
cccskills
무료GitHub에 공개한국어 소개

split-micro-app

React Router 기반 독립형 마이크로 앱의 분리, 빌드, 배포와 유지 관리 방법을 다룹니다. SSR·SSG, Cloudflare Workers, CDN 자산, 게이트웨이 경로, 미리보기 구성과 Worker 서버 렌더링 번들 크기 관리도 안내합니다.

원문원문 설명 보기

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.

lobehub/lobehub스타 83,500(GitHub 83,500 + cccskills 0)
연동 도구CloudflareDocker

실행 점수 미검증

설치 방법 보기

이럴 때 유용합니다

  • 큰 앱에서 독립 화면 분리
  • React Router SSR 또는 사전 생성 구성
  • Worker SSR 번들 크기 줄이기
  • CDN 배포와 미리보기 환경 정리

한국어 소개

할 수 있는 일

이 스킬은 React Router를 사용하는 독립형 마이크로 앱을 분리하고 빌드, 배포, 유지 관리하는 설계 지침을 제공합니다. 서버 렌더링(SSR), 미리 만든 페이지를 제공하는 SSG, Cloudflare Workers, CDN이나 R2에 자산 배치, 게이트웨이 경로, Docker 배포를 다룹니다. 오픈 소스 저장소와 클라우드 호스팅 환경 사이에서 코드와 빌드 책임을 나누는 방법, 미리보기 Worker 구성, CI에서 배포 대상을 정하는 방식도 설명합니다. SSR 번들에 불필요한 의존성이 들어간 경우 모듈이 유입된 경로를 추적하고, 클라이언트에서만 기능을 불러오거나 SSR 대체 모듈을 사용하는 방법으로 크기를 줄이는 방안을 살펴봅니다.

이럴 때 유용합니다

큰 앱의 일부 화면을 분리하려는 경우나 SSR을 유지하면서 Workers에 배포하려는 경우에 참고할 수 있습니다. 오픈 소스 버전과 호스팅 환경의 오버레이를 함께 운영하는 팀, 미리보기와 정식 배포 경로를 정리하려는 팀에도 유용합니다.

사용 예시

  • “이 화면을 독립형 React Router 앱으로 분리하는 구성을 알려 주세요.”
  • “Worker SSR 번들이 큰 원인을 추적하는 방법을 설명해 주세요.”
  • “Docker 배포와 클라우드 배포의 빌드 경로를 정리해 주세요.”

주의 사항

제공된 자료는 일부만 포함하므로 소개는 확인 가능한 내용에 한정됩니다. 일부 안내는 React Router 8, Vite 8, Cloudflare Vite 플러그인을 전제로 합니다. 클라우드 기능 일부와 배포에는 별도 저장소의 설정, Cloudflare 계정, CI 비밀 값이 관련됩니다.

이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.

문서 점검

AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.

안전성전송·삭제·명령 실행 등을 명시하고 있는가
B일부 부족클라우드, Docker, CI 배포 경로를 다루지만 확인 절차는 일부만 제시합니다.
도입의 용이성필요한 도구나 API 키가 적고 적혀 있는가
B일부 부족React Router 8, Cloudflare 계정, 별도 저장소 설정이 언급되어 있습니다.

점수에는 넣지 않았습니다. 2026년 10월 26일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.

평가가 다르다고 생각되면 재검토를 요청하기

어느 항목이 왜 다르다고 생각하는지 적어 주세요. 로그인은 필요 없습니다. SKILL.md와 대조해 평가를 다시 만들고 이곳에 반영합니다.

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

포함된 파일(1)

  • SKILL.md36.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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 (/acceptance, /agent/:aid/docs/:docId; /verify only redirects retired report links into 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.

SurfaceWho serves itBuild
CloudGateway (torii) → Worker (SSR)Cloud Next does not build:spa:<name>, copy _spa-<name>, rewrite to /spa-<name>, or CDN-upload that prefix
OSS / 子部署 DockerNext in the same image, client-renderedbuild: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)

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 / cutHow
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 hubsvite 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 lambdaClient @/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 langsOn 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 CDNOn 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 featuresContext seam owned by the shared feature (see src/features/Acceptance/Viewer/Conversation/originConversation.tsx); app provides, micro app leaves null → affordances hide
Bypass barrelsDeep-path imports (@/features/Acceptance/Acceptance, not the barrel) — barrels evaluate sibling exports with side effects
Compose capability atomsShared 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 componentsLift 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 deployed worker — OSS verify-share.yml + cloud verify-share.yml.

Previews upload to a sibling Worker, never to the production script. Cloudflare only rolls back to the 100 most recently uploaded versions of a script, and preview uploads count: at share's PR rate (10 uploads in under an hour on a busy day) every real deployment left the window within a day, which broke the gateway admin's (鳥居番) rollback list. So every verify passes --name lobehub-<name>-preview, and the production script's version list holds only deployments. Two consequences: the preview Worker must exist before the first upload (wrangler versions upload refuses a never-deployed script — bootstrap it once with wrangler deploy --name lobehub-<name>-preview from any stub; the next upload replaces it), and the preview API token must be allowed to edit the -preview name. Preview URLs are then https://<alias>-lobehub-<name>-preview.lobeobjects-tg.workers.dev, and VITE_CDN_BASE must point at that same origin. Alias namespaces must not collide on the shared preview worker: OSS uses pr<N>, cloud uses cloudpr<N>. Manifest source differs by what the repo can read: cloud verify borrows the deploy's share-deploy-state artifact (same repo); OSS verify cannot read cloud artifacts, so it self-bootstraps from its own last successful run's share-build-inputs (first run always builds).

1b. When the surface renders Cloud-only code

lobehub-cloud includes this repo as a submodule at lobehub/ and shadows it path-by-path through tsconfig paths (@/business/* → ./src/business/* then ./lobehub/src/business/*; @/* → ./src/* then ./lobehub/src/*). Split by ownership, not by which repo is handy:

apps/auth is the cheap case: because ssr: false ships no render graph, the Cloud overlay (BusinessAuthProvider → Turnstile + referral) added one chunk and 0 extra SSR stubs — measured at 585.7KB gz eager vs 587.3KB for the open-source build, with byte-identical markup. Do not assume that; apps/share needed 8.2MB of stubbing. Measure per app.

CodeWhere it goes
Routes, shell, SSR pipeline, worker, CDN deploy, SSR stubs of OSS modulesOSS apps/<name> — Docker serves the same app
Rendering surfaces only Cloud implementsStays in Cloud. OSS keeps the @/business/* stub (return null / passthrough, real types); the app imports the seam
SSR stubs for Cloud-only store hubsCloud (apps/<name>/stubs/*), injected into the shared config

The RR config is a factory so both hosts share one pipeline: apps/share/vite.config.shared.mts exports createShareRrConfig({ appRoot, extraSsrStubs, repoRoot, resolvePlugins }), and the OSS vite.config.rr.mts is a thin caller reading SHARE_TSCONFIG_PROJECT / SHARE_EXTRA_SSR_STUBS. Cloud fills both in scripts/shareApp.ts and drives the submodule app through scripts/{buildShare,devShare,deployShare,shouldBuildShare}.ts.

Landmine — the overlay silently doesn't apply. Vite 8's native resolve.tsconfigPaths resolves against the tsconfig nearest each importer, so submodule files resolve through the submodule's own tsconfig and the host overlay is lost. The build still succeeds and ships the open-source fallback surfaces (for share: a blank page). An overlay build must use the vite-tsconfig-paths plugin pinned to the host tsconfig and switch the native one off (tsconfigPaths: !resolvePlugins). Both settings are right in their own context — native for the standalone build, plugin for the overlay build. Verify by grepping build-inputs.txt for a Cloud-only file.

Whoever's code must be inside the artifact owns the build. Share deploys from lobehub-cloud (.github/workflows/deploy-share.yml), not OSS. Never add a deploy workflow to a repo that can only produce the fallback.

OSS PRs can still verify with the real overlay. Same-repo OSS PRs clone the overlay repo @ HEAD via .github/actions/business-overlay (the clone+overlay step extracted from desktop-build-setup: overlay files land in $GITHUB_WORKSPACE/.., which works because the repo is named lobehub so the checkout already sits at the submodule path), then cd .. && pnpm install and run the overlay repo's own bun run build:share — the tsconfig / stub knowledge stays over there. The OSS workflow never hardcodes the private repo name: it comes from the Actions repository variable OVERLAY_REPOSITORY; the token reuses the pre-existing LOBEHUB_CLOUD_TOKEN secret (deliberately not renamed — the desktop release workflows already reference it, and a rename would mean reconfiguring the org secret). When either is unset (fork PRs always), the workflow falls back to the OSS-stub build + wrangler dry-run as a pure compile/size guard. Keep new public-facing CI wording on the neutral "business overlay" vocabulary — the older desktop release workflows still leak the internal naming and are the known remaining exception. One trap: an overlay build's build-inputs.txt is overlay-root-relative (repoRoot = dirname(SHARE_TSCONFIG_PROJECT)), so OSS files appear as lobehub/src/... — strip that prefix before exact-matching against the OSS repo's own diff (sed 's#^lobehub/##' in the verify workflow); the meta triggers already match both spellings.

Cloud affected-detection needs submodule history: a bump is a single lobehub entry in the host diff, so the workflow runs git -C lobehub fetch --unshallow and compares the previous submodule SHA (carried in the state artifact) before diffing against build-inputs.txt.

Re-run the module trace in the Cloud build — the overlay adds chains OSS never sees (ShareAppShell → @/store/serverConfig/Provider → … → @/store/workspace → workspaceBootstrap → @/store/home → @/store/chat cost 8.2MB gzip until stubbed).

Merge order: OSS PR → submodule bump → Cloud PR. Until the OSS PR lands, anything deployed from a local submodule mirror is not reproducible from any commit — say so when handing over.

2. Deploy: CDN Assets + Worker

Assets follow the cloud convention (resolveViteBase): stable prefix, no version stamp — VITE_CDN_BASE=https://web-assets.lobehub.com/<name>/; content-hashed filenames make uploads incremental and immutable. The prefix axis is the app, nothing else: do not invent <name>-oss / <name>-cloud variants to separate build origins — hashed filenames already make one prefix safe for all of them. ASSET_S3_PUBLIC_DOMAIN is an origin; any path segment belongs on ASSET_BASE_URL instead. bun run deploy (see apps/workbench/scripts/deploy.ts) = build with CDN base → upload build/client/assets to R2 (web-assets bucket, LobeHub account) → wrangler deploy. R2 creds: 1Password Shared vault item "CI R2 - web-assets". CI injects repo secrets ASSET_S3_* (ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET, ENDPOINT, REGION, PUBLIC_DOMAIN). Local bun run deploy can fall back to MOBILE_S3_*. Never echo values.

Worker responsibilities beyond SSR: reverse-proxy /api|/oidc|/trpc|/webapi to WORKBENCH_API_BASE (standalone mode; behind the gateway the browser hits the apex and the gateway routes API to app instead), and redirect / + unknown paths to WORKBENCH_APP_HOME.

3. SSR

  • Loader-side data: per-request tRPC client (httpLink + superjson + forwarded cookie header, base from wrangler vars via cloudflareContext) — the browser lambdaClient is SSR-stubbed. Inject into SWR with <SWRConfig value={{ fallback: { [unstable_serialize(key)]: data } }}>; key must equal useClientDataSWR's (augmentKey passes through when workspaceId is null).
  • isLoading && !data: with SWRConfig fallback, isLoading is true during mount revalidation — a bare if (isLoading) swaps SSR content for a spinner after hydration.
  • i18n: narrow to the namespaces the app renders; root loader preloads locale resources, i18n inits sync (initAsync: false) with bundled resources; SSR env stubs the shared glob loader to only its namespaces (full glob = every locale × ns as worker chunks).
  • CSS two layers: @lobehub/ui/theme.css + global.css + style.css imported by the theme shell, which RR emits as route <link>s → extractStaticStyle(html) in entry.server.tsx for emotion. Non-streaming render (await stream.allReady) so extraction is complete.
  • <html suppressHydrationWarning> — next-themes stamps data-theme pre-hydration.
  • Time-dependent output must be client-gated: relative timestamps and anything else the server and client compute differently belong behind a useHydrated() (useSyncExternalStore) check, not in the SSR pass.
  • Skeletons must not swallow SSR'd chrome — and must hold its position. A list that flips to a skeleton on mount blanks the server-rendered header with it: render the header slot in the skeleton branch too. Presence is not enough — every phase (no-JS SSR fallback, post-hydration skeleton, settled list) must put that chrome in the same layout container as the final render, or the page jumps sideways when JS lands. Share's hero shipped left-flush while the settled list centered it in a min(960px, 100%) column, twice: the SSR fallback rendered ShareHero bare, and ChatList's showSkeleton branch rendered headerSlot outside WideScreenContainer. Verify by measuring the element's getBoundingClientRect().x across all three phases (stall the messages request to pin the skeleton phase), not by eyeballing one screenshot.
  • Preload the error namespace, not just the render ones. The boundary shows exactly when a chunk failed to load — precisely when the client can no longer fetch a dictionary — and an untranslated boundary prints raw keys (error.title…). Cost for share: +6.3KB gzip/document.
  • Prefilling a virtualized list: virtua caches measured sizes by index, so a truncated prefill that later grows misaligns every row. Prefill the whole list or none of it.

3b. Prerendering instead of SSR (ssr: false)

When every page is config-free at render time, ssr: false + prerender drops the whole SSR bundle-weight battle: no stubs, no client gates, and the worker is a static picker. apps/auth's is ~7KB — it resolves the locale, fetches one document out of the assets binding, and swaps a window.__SERVER_CONFIG__ placeholder for the deployment's config.

  • Runtime config cannot be baked. The build has no deployment's env, so the document is prerendered config-free and the worker injects it. Anything that reads it must be hydration- gated (useIsHydrated), or the first client render diverges from the document.
  • Audit what actually needs to travel that way — most of it does not. The hydration gate applies to the whole injected object, so every value routed through it is a value missing from the prerendered HTML. In apps/auth the config carried 11 fields; the pages read 6, and the most visible one was in the wrong layer entirely: enableBusinessFeatures is a build-time constant (@lobechat/business-const, false open-source / true in Cloud via a pnpm override) that was being round-tripped through the server, and with it the SSO provider list — itself a hardcoded array in the Cloud overlay. Reading the constant directly instead put Google/GitHub/Apple into the static document. featureFlags, enableMarketTrustedClient, telemetry and aiProvider had zero consumers and were dropped from the endpoint, which is public and unauthenticated. Do this audit before accepting "config-dependent UI pops in after hydration" as the cost of prerendering.
  • Prerendering turns every localStorage read during render into a hydration bug. A returning user's stored state (useState(readStored)) makes the first client render disagree with a document that was built without it — apps/auth hit this twice, on the terms checkbox and on the "last used" provider badge, both of which also reorder the buttons. Move them to useState(empty) + useEffect. A fresh browser will not reproduce it; seed the keys and reload.
  • The config needs an endpoint. A worker has no access to the app's env; apps/auth reads GET /webapi/auth/spa-config (public, s-maxage) through the API base and tolerates failure by leaving the placeholder in place.
  • Landmine — one document per locale must live at the canonical path. Prerendering /:locale/signin and serving it at /signin looks like it works and silently breaks hydration: React Router matches the plain route, finds no context for the prefixed route id, and React re-renders the whole document — leaving two copies of the page in the DOM, with no console warning. Only the second one is visible, so a screenshot looks fine; count document.querySelectorAll('form') to catch it. The fix is one build pass per locale (AUTH_PRERENDER_LOCALE → environments.ssr.define), each prerendering the same canonical paths, with the non-default passes' documents folded into build/client/__i18n/<locale>/. Keep the locale out of the client define so every pass emits byte-identical assets — scripts/build.mjs asserts that by resolving each copied document's asset refs against the default build.
  • The matrix costs build time, not bytes — and it parallelizes. apps/auth runs all 18 locales for 73 documents and 17.8MB in build/client, with no change to first load (640.8KB gz over the same 34 eager files: the per-locale dictionary chunks are lazy and each document carries only its own). The non-default passes share nothing and write to their own build-<locale>, so they fan out: 3m05s sequential → 44s at 6 concurrent on 16 cores. Each pass costs about two cores; past ~6 it stops paying (9 concurrent measured 42s, with per-pass time rising 12s → 16s). AUTH_BUILD_CONCURRENCY overrides the min(6, availableParallelism() - 2) default, which lands on 2 for a 4-vCPU CI runner — budget ~2min there, and mind that NODE_OPTIONS=--max-old-space-size applies per child. If that is still too slow, the remaining move is one build plus N renders against build/server/index.js with the locale threaded through entry.server.tsx — that trades the build-time define for request state, so it needs AsyncLocalStorage or strict sequencing. Verify RTL locales (ar, fa-IR) in a browser — they exercise the <html dir> path that no LTR document touches.
  • The prerender list is loaded by plain Node (react-router.config.ts), so it cannot import anything alias-resolved and has to be a literal array. Guard it against the dictionaries on disk in the build script, or a newly translated locale silently prerenders as English.
  • i18n must be synchronous for the served locale. A resources-to-backend fetch renders English first and swaps a tick later, which is a hydration mismatch against a prerendered zh-CN document. Bundling every locale into the client instead costs a lot (auth: 63KB gz of dictionaries, eagerly modulepreloaded on every route), so apps/auth inlines only the served locale into the document as <script type="application/json"> and keeps the others behind an on-demand backend for the language switcher. Weigh it: that moved 44KB gz off the JS path but put 22KB gz onto the document, which is the one thing first paint waits for, and the dictionary is now re-sent per document instead of cached once. A per-locale hashed JS file loaded by a blocking script would beat both; nobody has built it yet.
  • The client build must stay locale-agnostic for the one-pass-per-locale scheme to share assets, so anything that reads the dictionaries needs a server/client twin: the prerender module holds every bundle, the .client twin reads the document's inlined JSON, and vite.config.shared.mts swaps them per environment. meta() runs on both sides — it goes through the same twin, not a second copy of the strings.
  • resolve.noExternal: true on the ssr env, build only. The prerender pass runs the built server bundle through plain Node, which rejects the extensionless directory imports some published es/ bundles ship (@lobehub/fluent-emoji). Setting it for serve too breaks the dev module runner on inlined CJS (module is not defined, then require is not defined) — and leaving it off leaves react-router dev 500ing on that same directory import. Known gap: apps/auth's dev server does not run; the working local loop is bun run build && wrangler dev.

RR v8 gotchas (docs/templates still say v7):

v8 changeSymptom
load context must be new RouterContextProvider()template's plain object → 500
MetaArgs.data renamed loaderData{ data } destructure silently undefined; build passes
* splat does not match bare /no index route → root falls into ErrorBoundary
vite preview ignores server.proxyloader self-fetch gets HTML → keep API base explicit

4. SEO

One shared builder (app/lib/seo.ts → buildPageMeta): title, description, robots, og:title/description/type/site_name/locale (underscore form)/image(+alt), twitter card set. Rules: leaf meta fully replaces root meta — every leaf returns the whole set; og:image must be an absolute URL (reuse landing's https://lobehub.com/assets/cao-og.webp); dynamic title/description come from the route loader (subject title · BRANDING_NAME, requirement text truncated ~200 chars).

5. Gateway Routing (torii, ../lobehub-gateway)

Config = KV config:lobehub.com, mirrored in config/entrypoints/lobehub.com/<env>.json. To add a micro app:

  1. targets.<name> = worker hostname (must be cross-zone, e.g. *.workers.dev; https).
  2. targetPolicies.<name> — cookies if SSR loaders need the .lobehub.com session forwarded; none for public-only; all adds Authorization. Add "locale": "query" when the app reads ?hl= (share does).
  3. rules — first-match path prefixes ({ "match": "/acceptance", "target": "<name>" }). Leave /trpc unruled: it falls to default (app) so browser API calls stay same-origin authenticated. .data suffix is normalized before matching.
  4. Validate: bun run test in the gateway repo (invariant suite reads the mirror).
  5. Staging: bun scripts/torii.ts push --env staging --expect <fp> (needs TORII_ACCESS_*), or poke staging KV directly (wrangler kv key put --namespace-id <staging CONFIG ns>) — README-sanctioned. Prod writes only via the Toriiban (鳥居番) admin Promote button: torii.ts promote prints the exact delta and refuses to write, and push --env prod --confirm-prod exists but bypasses a deliberate human gate — don't. Before promoting, pull --env prod (repo mirrors go stale — a mirror-vs-live diff is not drift), run the invariant suite, and re-run verify --env staging.
  6. Verify with staging debug headers: x-torii-target / x-torii-decision per request, and test document + .data + unaffected routes.
  7. Rollback and previews live in 鳥居番's target detail pane, in a Worker card that every target whose host is in the gateway repo's bindings.ts gets for free. Releases lists the production script's Cloudflare deployments; a row is only rollbackable while its version is within Cloudflare's 100-upload window — which is the reason previews stay off the production script (§1). Previews lists the -preview Worker's versions (PR label and actor come from the --message stamp) with Open and, under staging, Set override, which writes the preview host into the target's targetOverrides immediately (a saved, history-recorded config write — no editor Save). Adding a micro app to bindings.ts is therefore part of wiring it into the gateway.

Landmine (fixed 2026-08, stay aware): the lobehub-com react-router-data plugin owns .data protocol affinity for the landing pair; it consults resolveRule and lets other targets' .data fall through — if a future entrypoint clones that plugin, keep the fall-through.

Checklist for a new micro app

  1. Decide ownership first (§1b): does the surface render Cloud-only code? That fixes which repo builds, deploys, and holds the extra SSR stubs.
  2. Scaffold apps/<name> (copy workbench/share shapes); routes + root + entry.server + worker.
  3. Build, run the module trace, cut SSR weight (gate/stub/slot/deep-import) until gzip sane — in the overlay repo too, if there is one.
  4. Wire CDN deploy (deploy.ts, stable _<name>/ prefix) + wrangler vars + redirects, plus a PR-time verify workflow in every repo whose changes reach the artifact (§1 PR-time verify — preview version on the bootstrapped lobehub-<name>-preview Worker, size guard, non-colliding alias namespace).
  5. Loader data + SWR fallback + meta builder + i18n narrowing (+ error); verify with /trpc-blocked browser run (content must survive) and view-source (SSR content + meta present).
  6. Deploy worker; verify workers.dev standalone (API proxy, / redirect).
  7. Gateway: target + policy + rules on staging KV; curl matrix with x-torii headers; promote.
  8. OSS Docker: all five links in the Hosting section, plus the removal sweep from the main SPA.
  9. Cloud Next: no Vite target, no /spa-<name> rewrite, no _spa-<name> CDN upload.

Verifying, before you tell anyone it works

Build success proves nothing about what rendered. Each of these produced a real defect that a green build hid: the overlay resolving to OSS stubs, the dev shell answering 200 with the wrong app, an edit that silently no-op'd because the anchor string had drifted (deployed a 500 — so: build → local wrangler dev → browser → then deploy), and a "fix" asserted from the source instead of the page. Prove the Docker chain by running build:spa:<name> + <NAME>_REQUIRED=1 build:spa:copy and asserting every asset the generated template references exists under public/; prove the SSR page by viewing source, not by reading the loader.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

acceptance

무료요약 있음

실제 제품을 적절한 조작 환경에서 실행해 변경 사항을 검증하고, 사용자가 확인할 수 있는 증거를 모아 인수 보고서로 게시합니다. 기존 검증 계획을 확인하거나 점검 항목을 만들고, `lh` CLI로 결과를 게시할 수 있습니다.

  • 변경된 제품 동작을 실제로 확인하기
  • 스크린샷 등 검증 증거 수집하기
  • 검증 계획과 보고서 작성하기
lobehub/lobehub8.4만2026년 10월 26일 업데이트

add-model-provider

무료요약 있음

LobeHub에 AI 모델 제공자를 추가하는 과정을 안내합니다. 런타임 연동, 모델 정보와 설정, 브랜드 표시, 문서화 및 인수 확인을 다루며, 기존 제공자에 모델만 추가하는 경우와 새 연동이 필요한 경우를 구분합니다.

  • LobeHub에 새 AI 모델 제공자 추가하기
  • 제공자의 설정 안내나 설명 갱신하기
  • 모델 추가에 새 제공자 연동이 필요한지 판단하기
lobehub/lobehub8.4만2026년 10월 26일 업데이트

add-setting-env

무료요약 있음

서버 환경 변수로 사용자 설정의 기본값을 지정하도록 구현하는 절차를 안내합니다. 변수 검증, 서버 설정 구성, 사용자 저장소 반영부터 예시 환경 파일과 셀프 호스팅 문서 갱신까지 다룹니다.

  • 환경 변수로 사용자 설정 기본값 지정
  • 설정을 서버 구성과 사용자 저장소에 연결
  • 환경 변수 예시와 문서 갱신
lobehub/lobehub8.4만2026년 10월 26일 업데이트

agent-runtime-hooks

무료요약 있음

에이전트 실행 단계, 도구 호출, 사람의 승인, 컨텍스트 압축, 하위 에이전트 호출에 훅을 등록하는 방법을 안내합니다. 이벤트를 관찰하고 로컬 도구 결과를 모의하거나 설정에 따라 웹훅으로 도구 호출을 허용 또는 거부할 수 있습니다.

  • 에이전트 단계와 오류 기록
  • 테스트 중 도구 결과 모의
  • 웹훅으로 도구 호출 허용 또는 거부
lobehub/lobehub8.4만2026년 10월 26일 업데이트

agent-signal

무료요약 있음

에이전트의 이벤트 기반 백그라운드 작업을 설계하고 구현하도록 돕습니다. 소스, 시그널, 액션, 정책의 역할을 나누고 비동기 처리, 중복 실행 방지, 완료 기록과 관측 기능을 다룹니다.

  • 이벤트로 시작되는 백그라운드 작업 추가
  • 대화 요청과 작업을 분리
  • 액션의 중복 실행 방지
lobehub/lobehub8.4만2026년 10월 26일 업데이트

agent-testing-bot

무료요약 있음

실제 데스크톱 채팅 앱에서 봇 채널의 인수 테스트를 진행합니다. Discord, Slack, Telegram, WeChat, Lark, QQ에서 메시지를 보내고 응답을 기다린 뒤 결과 화면을 캡처합니다. iMessage는 BlueBubbles 브리지를 사용합니다.

  • 실제 Discord 채널에서 봇 응답 확인하기
  • Slack이나 Telegram 대화 경로 테스트하기
  • macOS 앱의 작업 결과 화면으로 기록하기
lobehub/lobehub8.4만2026년 10월 26일 업데이트

lobehub의 스킬 모두 보기

이 스킬의 문제 신고