Demo

How to create a public page

Register a blank-layout page outside the ERP chrome with AppRegistry.rootRoute.add() — single screens or nested customer portals with React.lazy. Do not use route.add().

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
TSXfrontend/src/index.tsx
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)
TSfrontend/src/index.tsx — shell
AppRegistry.rootRoute.add({ path: `/${metadata.name}/*`, element: <PublicShellPage />, });
TSXfrontend/src/page/public/public-shell.page.tsx
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