Local development
This page is for developers who want to run the admin locally with hot module reload, write plugins or themes against live admin code, or contribute to the CMS itself.
If you just want to run the admin without modifying it, the no-build path is faster.
Prerequisites
- Node.js 20+ (the project uses modern Node APIs and tests)
- Git
- A Firebase project + Flexweg account configured (same as the no-build path — see Installation overview)
Clone and install
git clone https://github.com/<owner>/flexweg-cms.git
cd flexweg-cms
npm install --legacy-peer-deps
--legacy-peer-deps is requiredThe project pins TypeScript 6 (matching the kanban sibling project's convention), while react-i18next declares an optional peer on TypeScript 5. The peer is optional but npm 7+ treats peer ranges strictly by default. The flag is safe — install completes correctly.
If you forget the flag you'll get an ERESOLVE error. Just re-run with --legacy-peer-deps.
Configure .env
Copy the example and fill in your Firebase credentials:
cp .env.example .env
Edit .env:
VITE_FIREBASE_API_KEY=AIza…
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=1234567890
VITE_FIREBASE_APP_ID=1:1234567890:web:abcdef
# Email of your bootstrap admin (must match the Firebase Auth user
# AND the email pinned in Firestore rules)
Vite picks these up automatically and bakes them into the bundle.
Run the dev server
npm run dev
The admin runs at http://localhost:5173. Open it in your browser, log in with your bootstrap admin email + password.
The first time you run the admin (locally OR in production), you'll need to fill in the Flexweg API key + site URL under Settings → General. These live in Firestore (config/flexweg), so once saved they apply to both your local dev session AND any production deploy of the same Firebase project.
What npm run dev does
- Prebuild Tailwind themes — runs
scripts/build-theme-tailwind.mjswhich compilesmagazineandcorporatethemes' Tailwind CSS intotheme.compiled.css(since they use a Tailwind preprocessor, not Vite's PostCSS). Default theme uses SCSS, handled separately. - Vite dev server — HMR on. Editing any
.tsx/.tsfile rebuilds the changed module and pushes it to the browser without reload.
In dev mode, plugins and themes live as static imports in src/plugins/index.ts and src/themes/index.ts. They're bundled into the main admin chunk, so HMR works for them too. The runtime external loader (which fetches Firestore for the registry) is bypassed in dev — import.meta.env.DEV evaluates to true and the BUILTINS_DEV array populates PLUGINS / THEMES directly.
Run tests
npm test # one-shot
npm run test:watch # watch mode
The test suite is Vitest. Tests live next to the code (*.test.ts, *.test.tsx). 100+ unit tests cover slug rules, plugin registry, block registry, image processing, menu resolver, etc.
Type-check
npm run typecheck
Runs tsc --noEmit against the entire codebase. The build pipeline runs this automatically (prebuild script), so a npm run build that succeeds means typecheck passed.
Build for production
npm run build
This runs:
npm run typecheck— guarantees the build is type-cleannpm run themes:tailwind— compile Tailwind themesvite build— producedist/admin/(the admin SPA)node scripts/build-themes.mjs— copy compiled theme CSS intodist/theme-assets/node scripts/build-bundled-externals.mjs— emit each in-tree plugin / theme as a separate ESM bundle underdist/admin/{plugins,themes}/<id>/
Output structure:
dist/
├── admin/ ← upload this to /admin/ on Flexweg
│ ├── index.html
│ ├── config.js ← Firebase config (null stub if no .env)
│ ├── external.default.json ← bundled plugins/themes baseline
│ ├── assets/ ← admin bundle
│ ├── runtime/ ← runtime stubs for external bundles
│ ├── plugins/<id>/bundle.js ← per-plugin ESM bundle
│ └── themes/<id>/bundle.js + theme.css
└── theme-assets/ ← upload this to /theme-assets/ on Flexweg
└── <theme-id>.css ← public-facing theme CSS
Daily dev workflow
npm run devkeeps running in a terminal.- Edit code, the browser reloads.
- Before committing, run
npm test && npm run typecheckto catch regressions. - To deploy:
npm run build, uploaddist/admin/anddist/theme-assets/to your Flexweg site.
Adding a new in-tree plugin
- Create
src/plugins/<plugin-id>/. - Add
manifest.ts(ormanifest.tsx) that default-exports aPluginManifestobject. Use@flexweg/cms-runtimeimports, NOT relative paths to admin internals. - Append the import + array entry in
src/plugins/index.ts. - Append the metadata to
PLUGINSinscripts/build-bundled-externals.mjsso the build script knows to compile it.
// scripts/build-bundled-externals.mjs
const PLUGINS = [
{ id: "core-seo", name: "Core SEO", version: "1.0.0" },
{ id: "your-plugin", name: "Your Plugin", version: "1.0.0" }, // ← add
// …
];
- Run
npm run dev— HMR picks up your changes immediately. - When ready:
npm run buildproduces a separate bundle for your plugin underdist/admin/plugins/<plugin-id>/. It'll be listed inexternal.default.jsonautomatically.
See [Creating plugins] for the full tutorial.
Adding a new in-tree theme
Same idea: src/themes/<theme-id>/, manifest at the root, append to src/themes/index.ts and the THEMES array in scripts/build-bundled-externals.mjs. See [Creating themes].
File watching gotchas
- Vite HMR watches
src/. Changes topublic/,index.html,vite.config.tsorpackage.jsonrequire a server restart. - Tailwind compilation is not part of HMR for in-tree themes. If you edit
src/themes/magazine/theme.css, runnpm run themes:tailwindmanually OR runnpx tailwindcss -c <config> -i <input> -o <output> --watchin a second terminal — Vite picks up the rewritten output via its file watcher. - The runtime stubs in
public/runtime/*.jsare static files served as-is. Editing them requires a manual reload.
Continue
- Deployment — upload to Flexweg
- [Architecture] — how the admin fits together
- Creating plugins
- Creating themes