Installation Guide — UI07 Ember
Prerequisites
-
Node.js 22+ (LTS recommended)
-
pnpm 10+ — the kit pins
packageManager(pnpm@10.33.4) inpackage.json, so the recommended path is Corepack (ships with Node, no global install needed):bashcorepack enable # `pnpm` is provisioned automatically on first invocationIf you prefer a manual global install:
bashnpm install -g pnpm -
A code editor with TypeScript support (VS Code recommended)
Setup
# 1. Copy environment variables (kit builds without them — all optional)
cp .env.example .env.local
# 2. Install dependencies
pnpm install
# 3. Start development server
pnpm dev
Open http://localhost:3000 in your browser.
Environment Variables
The kit ships with no required environment variables — it runs fully on mock data.
Create .env.local from .env.example only when you wire optional integrations. The
shipped .env.example documents three variables, all optional:
# Optional: Google Analytics 4 measurement ID. Loaded by AnalyticsScripts
# only after the user grants analytics consent.
NEXT_PUBLIC_GA_ID=
# Optional: demo API key used in code snippets and dashboard examples only.
EMBER_API_KEY=
# Template Empire live-demo flag — DO NOT SET in your own deployment.
# Used only by the templateempire.io live preview to render a "Demo" banner
# and apply noindex/nofollow. Leaving it blank dead-code-eliminates the
# banner from the buyer bundle.
NEXT_PUBLIC_TE_DEMO=
When you add new env vars, document them in .env.example first so a buyer
doing cp .env.example .env.local can see what to fill in.
Build for Production
Network dependency at build time: The kit loads its Geist body font via
next/font/google. Duringpnpm build, Next.js downloads it from Google Fonts (fonts.googleapis.com,fonts.gstatic.com). The Cabinet Grotesk display font is self-hosted (next/font/local,src/app/fonts/CabinetGrotesk-Variable.woff2) and needs no network access. In CI environments with restricted egress or air-gapped builds, the Geist download will fail. Options:
- Allow outbound HTTPS to
fonts.googleapis.comandfonts.gstatic.com- Switch Geist to a self-hosted file: replace its
next/font/googleimport withnext/font/localand add the font file tosrc/app/fonts/. See CUSTOMIZATION.md for guidance.
pnpm build
This generates a static export in the out/ directory (output: 'export',
trailingSlash: true, unoptimised images — configured in next.config.ts).
Preview Static Build
pnpm start runs next start, which expects a server build and will not serve the
static out/ directory. To preview the production export locally, serve out/ with a
static file server:
pnpm build
pnpm dlx serve out
# opens a local static server for the generated out/ directory
Use this to verify the production export before deploying.
Phase Gate Commands
Run these before every release:
pnpm typecheck # TypeScript strict check
pnpm lint # ESLint
pnpm build # Production static export
pnpm audit --prod --audit-level=high # Dependency audit (production deps only)
pnpm dev # Manual smoke test
The kit ships no test suite by default — wire your own (Vitest, Playwright, etc.) if your release process requires one.
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.
Troubleshooting
Port already in use
pnpm dlx kill-port 3000
pnpm dev
Tailwind styles not applying
Make sure you have postcss.config.mjs with the @tailwindcss/postcss plugin configured.
TypeScript errors after install
pnpm typecheck
If errors persist after a dependency bump, delete tsconfig.tsbuildinfo and re-run. The
project uses strict: true in tsconfig.json.
Licence Key
The kit validates your licence key on pnpm dev and pnpm build via a
predev / prebuild hook (scripts/validate-licence.mjs). To activate:
- Open
licence.jsonin the project root. - Replace the
TE-XXXX-XXXX-XXXXplaceholder with your key from store.templateempire.io/account (the same URL the validation script prints). - Run
pnpm dev— the validation script will confirm the key is set.
The check is informational during development; the build will still complete with a warning if the key is absent, letting you evaluate the kit before purchasing.
Template Empire