Goal
Ship a page that opens without the ERP sidebar / admin shell — kitchen boards, QR menus, customer portals, and similar experiences that are not standard plugin admin pages.
- Blank layout via AppRegistry.rootRoute.add()
- Path always prefixed with /${metadata.name}
- Optional multi-page portal: one /* shell + nested Routes + React.lazy
- Public customers ≠ Quan ERP admin users — separate auth
1. Prerequisites
You need a plugin with a working frontend register() entry and the base stack running so you can open the public URL.
- Plugin scaffolded under plugins/<name>/ with frontend/src/index.tsx
- metadata.name matches the folder name
- Plugin built/watched and installed so register() runs
- Know whether you need one screen or a multi-page portal
2. Register a single public page
For one-off screens (kitchen board, public menu by id), call rootRoute.add() inside register() with a namespaced path and element. Never use AppRegistry.route.add() for blank layouts.
- path — `/${metadata.name}/…` or `/${metadata.name}/:id`
- element — your page component (can use @quan-erp/shared-ui)
- Do not call menu.add() for public pages — there is no ERP menu chrome
1import type { AppRegistryState, PluginModule } from "@quan-erp/shared-types";
2import metadata from "../module.metadata.json" with { type: "json" };
3import { FoodKitchenBoard } from "./page/kitchen/kitchen.page";
4import { FoodPublicMenu } from "./page/public-menu/public-menu.page";
5
6const Plugin: PluginModule = {
7 register(AppRegistry: AppRegistryState) {
8 AppRegistry.rootRoute.add({
9 path: `/${metadata.name}/kitchen`,
10 element: <FoodKitchenBoard />,
11 });
12
13 // Dynamic segment
14 AppRegistry.rootRoute.add({
15 path: `/${metadata.name}/:id`,
16 element: <FoodPublicMenu />,
17 });
18 },
19};
20
21export default Plugin;3. Nested public portal (multi-page)
For login / home / signup flows, register one splat shell (`/${metadata.name}/*`) and nest react-router-dom Routes inside. Lazy-load each nested page so chunks stay small.
- One rootRoute entry for the whole portal — do not register every nested path on AppRegistry
- Use React.lazy + Suspense with LoadingState from @quan-erp/shared-ui
- Nested paths are relative to the splat (login → /${metadata.name}/login)
AppRegistry.rootRoute.add({
path: `/${metadata.name}/*`,
element: <PublicShellPage />,
});1import { LoadingState } from "@quan-erp/shared-ui";
2import { lazy, Suspense } from "react";
3import { Navigate, Route, Routes } from "react-router-dom";
4
5const LoginPage = lazy(() =>
6 import("./login/login.page").then((m) => ({ default: m.LoginPage })),
7);
8const SignupPage = lazy(() =>
9 import("./signup/signup.page").then((m) => ({ default: m.SignupPage })),
10);
11const HomePage = lazy(() =>
12 import("./home/home.page").then((m) => ({ default: m.HomePage })),
13);
14
15const HOME_PATH = "home";
16
17export function PublicShellPage() {
18 return (
19 <Suspense fallback={<LoadingState />}>
20 <Routes>
21 <Route index element={<Navigate replace to={HOME_PATH} />} />
22 <Route path="login" element={<LoginPage />} />
23 <Route path="register" element={<SignupPage />} />
24 <Route path="home" element={<HomePage />} />
25 <Route path="*" element={<Navigate replace to={HOME_PATH} />} />
26 </Routes>
27 </Suspense>
28 );
29}4. Public customers ≠ admin auth
External customers are not Quan ERP users. Do not reuse core admin login, the base axios client, or localStorage tokens for public portals.
- Dedicated public axios instance with withCredentials: true
- Backend owns httpOnly cookies prefixed with ${metadata.name}
- Gate protected public pages on cookie-authenticated API success (401 → login)
- Optional in-memory Zustand profile store — never persist tokens
5. Verify
After watch + install (or hot reload), open the public URL in a browser.
- Single page — open /${metadata.name}/kitchen (or your path); no ERP sidebar
- Portal — open /${metadata.name}/login and navigate to home; nested routes work
- Hard-refresh if register() just changed
- Network tab: public APIs use your plugin cookies, not admin session
Common mistakes
- Used AppRegistry.route.add() — page still shows ERP chrome
- Path without /${metadata.name} — collisions with other plugins
- Multi-page portal missing /* on the shell path — nested routes 404
- Eager imports for every portal page — huge login chunk
- Hooks that assume admin login on a public customer page