Skip to main content
Skip to content

Last updated on 4 June 2026

Installation Guide — UI03 ShieldOps Cybersecurity

Prerequisites

  • Node.js 20 or higher (nodejs.org)
  • pnpm 10 or higher: npm install -g pnpm

Setup

1. Extract and install

bash
cd <extracted-folder>
cp .env.example .env.local
pnpm install

2. Licence key

Open licence.json and replace the placeholder key TE-XXXX-XXXX-XXXX with the key from your account at templateempire.io/account. Builds work without a real key but will print a yellow warning.

3. Run the development server

bash
pnpm dev

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

The dev server uses the default Next.js port (3000). Change it in package.json if needed.

4. Build for production

bash
pnpm build

The static output is written to the out/ directory.

Deployment

This template uses output: 'export' — fully static, no server-side runtime required.

Vercel

bash
# Vercel CLI is typically installed globally via npm (standard for CLI tools)
npm install -g vercel
vercel --prod

Set output directory: out · Build command: pnpm build

Netlify

toml
# netlify.toml
[build]
  command   = "pnpm build"
  publish   = "out"

Cloudflare Pages

Build command: pnpm build · Output directory: out

Network requirements

The kit uses Google Fonts (Geist, Geist Mono, Chakra Petch) loaded via next/font/google. The build process fetches font files from Google's CDN at build time. If you are building in a network-restricted or air-gapped environment, either:

  • Replace the next/font/google imports in src/app/layout.tsx with locally hosted fonts using next/font/local, or
  • Ensure your build machine has outbound HTTPS access to fonts.googleapis.com and fonts.gstatic.com.

Licence validation: When a real licence key is provided in licence.json, the prebuild script (scripts/validate-licence.mjs) makes an HTTPS request to templateempire.io to validate the key. In air-gapped or restricted-egress environments, either use the placeholder key (TE-XXXX-XXXX-XXXX) during local builds, or ensure outbound HTTPS access to templateempire.io is permitted before running pnpm build.

Common Issues

Port already in use

The dev server uses the default Next.js port (3000). Change it in package.json if needed:

json
"dev": "next dev --turbopack --port 3001"

tailwindcss not found

Run pnpm install from the template root.

Theme flash on load

The ThemeScript in providers.tsx prevents flash by reading localStorage before React hydrates. Ensure JavaScript is enabled.

GSAP SplitText

GSAP SplitText ships free with gsap 3.12+. If you see import errors, ensure you are on gsap ^3.15.0 as specified in package.json.

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.