Skip to main content
Skip to content

Last updated on 4 June 2026

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:

  1. npm registry - pnpm install pulls dependencies from registry.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

bash
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).

jsonc
// licence.json
{
  "key": "TE-YOUR-REAL-KEY-HERE",
  "domain": "yourbrand.com" // optional, used for licence-portal validation
}

3. Install dependencies

bash
# 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

bash
pnpm dev

Open http://localhost:3000 in your browser.

5. Build for production

bash
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

bash
# 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:

toml
# 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:

powershell
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:

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.ts headers(). For a static export, next.config headers() 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 _headers file, an nginx block — below) is explicit, portable, and what the examples here use.

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.com to script-src, https://www.google-analytics.com to img-src, and https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com to connect-src.
  • connect-src is 'self' only — add the specific origins your app actually calls (e.g. the GA hosts above). Avoid a blanket https:: it lets an XSS exfiltrate to any origin.
  • Clickjackingframe-ancestors 'self' blocks cross-origin framing and supersedes the legacy X-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-Protection is intentionally omitted (deprecated; CSP replaces it).
  • HSTSincludeSubDomains enforces HTTPS on every subdomain for the full max-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

Vercelvercel.json at the project root:

json
{
  "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:

nginx
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.