Installation Guide — UI02 Forge Developer Platform
Prerequisites
- Node.js 20 or higher (nodejs.org)
- pnpm 10 or higher
- OS: macOS, Linux, or Windows (PowerShell or WSL)
- Disk: ~500 MB free for
node_modules+.next+out
Network access
Only one blocking network dependency exists before the app can run:
- npm registry -
pnpm installpulls dependencies fromregistry.npmjs.org.
pnpm build does not fetch Google Fonts or any other external service. The default Geist + JetBrains Mono files are self-hosted in public/fonts/ and loaded with next/font/local.
The predev and prebuild licence hook may make a short, non-blocking request to https://templateempire.io/api/validate-licence after you replace the placeholder licence key. If that endpoint is unavailable or your CI is offline, the hook times out quickly and allows dev/build to continue.
For restricted or air-gapped environments, pre-cache npm by running pnpm install on an online machine, then copy the full project, including node_modules, to the offline target. pnpm install --offline will then work.
Setup
1. Set up environment
cp .env.example .env.local
2. Add your Template Empire licence key
Open licence.json and replace the TE-XXXX-XXXX-XXXX placeholder with the key you received at purchase. Replace the domain placeholder with your production domain, or set it to an empty string until launch. The predev and prebuild hooks run scripts/validate-licence.mjs and emit a warning until the placeholder is replaced (the warning does not block dev or build, but every run prints a reminder).
// licence.json
{
"key": "TE-YOUR-REAL-KEY-HERE",
"domain": "yourbrand.com" // optional, used for licence-portal validation
}
3. Install dependencies
# Enable pnpm via corepack (recommended):
corepack enable
corepack install
# Fallback: npm install -g pnpm
# Open the extracted project folder in your terminal, then:
pnpm install
4. Run the development server
pnpm dev
Open http://localhost:3000 in your browser.
5. Build for production
pnpm build
The static output is written to the out/ directory.
Deployment
This template uses output: 'export' — it produces a fully static site with no server-side runtime.
Vercel
# Install Vercel CLI
npm i -g vercel
vercel --prod
Set the output directory to out and the build command to pnpm build.
Netlify
Drag-and-drop the out/ folder into the Netlify dashboard, or:
# netlify.toml
[build]
command = "pnpm build"
publish = "out"
Cloudflare Pages
Set build command: pnpm build · Output directory: out
Any static host (Nginx, S3, etc.)
Serve the out/ directory as-is. All routes end in / due to trailingSlash: true.
Common Issues
tailwindcss not found during pnpm dev
The Turbopack config in next.config.ts pins the resolution root to the template folder (turbopack.root). If you still see a "cannot resolve tailwindcss" error — usually after a partial install, symlinked workspace, or stale cache — delete the local dependency state and reinstall:
Remove-Item -Recurse -Force node_modules, .next, out
pnpm install --frozen-lockfile
On macOS / Linux: rm -rf node_modules .next out && pnpm install --frozen-lockfile.
Port already in use
The dev server defaults to port 3000. Change it in package.json:
"dev": "next dev --port 4000"
Dark mode not activating
The theme is controlled by the light class on <html>. The inline script in layout.tsx restores the last-saved preference from localStorage. Ensure JavaScript is enabled.
Security Headers
Empire UI kits are static exports (output: 'export') — they emit HTML/CSS/JS with no runtime server, so the app sends no HTTP response headers of its own. Set security headers at your host/CDN (per-host examples below).
Set headers in your host's config, not
next.config.tsheaders(). For a static export,next.configheaders()is inert on a generic static host (S3, nginx, GitHub Pages) — it does anything only on Vercel, so relying on it is a false sense of security. Your host's native config (vercel.json, a_headersfile, an nginx block — below) is explicit, portable, and what the examples here use.
Recommended set
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
- CSP
script-src—'unsafe-inline'is required by the inline theme-flash-prevention script (it runs before hydration, so it can't be externalised without a flash);'unsafe-eval'by some animation-library code paths. A static export can't issue per-request CSP nonces. To harden, try removing'unsafe-eval'and test (modern GSAP/Three.js often don't need it), and replace the inline theme script with a hashed/external one to drop'unsafe-inline'. - Optional Google Analytics — if you wire up
NEXT_PUBLIC_GA_ID, GA is blocked by the CSP above unless you add its origins:https://www.googletagmanager.com https://www.google-analytics.comtoscript-src,https://www.google-analytics.comtoimg-src, andhttps://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.comtoconnect-src. connect-srcis'self'only — add the specific origins your app actually calls (e.g. the GA hosts above). Avoid a blankethttps:: it lets an XSS exfiltrate to any origin.- Clickjacking —
frame-ancestors 'self'blocks cross-origin framing and supersedes the legacyX-Frame-Options; unlike XFO it takes an allow-list (e.g.frame-ancestors 'self' https://your-other-site.com) if you embed your own site.X-XSS-Protectionis intentionally omitted (deprecated; CSP replaces it). - HSTS —
includeSubDomainsenforces HTTPS on every subdomain for the fullmax-age; only keep it if all your subdomains are HTTPS.
The public Empire UI demos deliberately ship no frame protection so our preview tooling + storefront can embed them. Your production site isn't embedded — keep
frame-ancestors 'self'(above).
Per host
Vercel — vercel.json at the project root:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
]
}
]
}
Netlify / Cloudflare Pages — put a _headers file in public/ (Next.js copies it into the out/ build output; don't edit out/ directly — it's regenerated each build):
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
nginx — inside your server { } block:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
After deploying, verify with securityheaders.com or DevTools → Network → Headers.