|
1 | 1 | --- |
2 | 2 | title: "Frontend Frameworks" |
3 | | -description: "Integrate React, Svelte, Vue, Unity, and other frameworks with ICP canisters" |
| 3 | +description: "Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using the asset canister and icp-cli" |
4 | 4 | sidebar: |
5 | 5 | order: 4 |
6 | 6 | --- |
7 | 7 |
|
8 | | -TODO: Write content for this page. |
| 8 | +ICP hosts frontend applications as asset canisters — static files (HTML, CSS, JavaScript) deployed onchain and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot. |
9 | 9 |
|
10 | | -<!-- Content Brief --> |
11 | | -Integrate popular frontend frameworks with ICP. Cover Vite plugin setup for React/Svelte/Vue, agent configuration with @icp-sdk/core, auth setup with @icp-sdk/auth, and hosting configuration. Also cover game engines (Unity WebGL, Godot HTML5) for onchain games. Show framework-specific starter templates. |
| 10 | +This guide shows you how to configure your framework's build pipeline, wire up the ICP JavaScript SDK, and deploy to an asset canister. |
12 | 11 |
|
13 | | -<!-- Source Material --> |
14 | | -- Portal: building-apps/frontends/existing-frontend.mdx |
15 | | -- JS SDK: @icp-sdk/core (https://js.icp.build/core), @icp-sdk/auth (https://js.icp.build/auth) |
16 | | -- Examples: hosting/react, svelte/svelte-motoko-starter, svelte/sveltekit-starter, hosting/godot-html5-template, hosting/unity-webgl-template |
| 12 | +## Prerequisites |
17 | 13 |
|
18 | | -<!-- Cross-Links --> |
19 | | -- guides/frontends/asset-canister -- deployment target |
20 | | -- guides/authentication/internet-identity -- auth setup per framework |
21 | | -- getting-started/project-structure -- Vite plugin in hello-world template |
| 14 | +- [icp-cli](https://cli.internetcomputer.org/) installed: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` |
| 15 | +- A backend canister deployed (or a static-only site with no backend) |
| 16 | +- Familiarity with [asset canisters](asset-canister.md) |
| 17 | + |
| 18 | +## The deployment model |
| 19 | + |
| 20 | +Every frontend framework integration follows the same pattern: |
| 21 | + |
| 22 | +1. Configure `icp.yaml` to point at your framework's build output directory |
| 23 | +2. Optionally add a Vite plugin (`@icp-sdk/bindgen`) to generate typed canister bindings at build time |
| 24 | +3. Use `@icp-sdk/core` in your app to read canister IDs and the root key at runtime from the `ic_env` cookie served by the asset canister |
| 25 | +4. Deploy with `icp deploy` |
| 26 | + |
| 27 | +The asset canister injects an `ic_env` cookie into every HTML response. This cookie carries the root key and any `PUBLIC_CANISTER_ID:<name>` environment variables you set — so your frontend never needs canister IDs baked into the build artifact. |
| 28 | + |
| 29 | +## React with Vite |
| 30 | + |
| 31 | +The [hello-world template](../../../getting-started/project-structure.md) uses React with Vite. It demonstrates the full stack: backend canister, auto-generated TypeScript bindings, and a React frontend that reads canister IDs at runtime. |
| 32 | + |
| 33 | +### icp.yaml |
| 34 | + |
| 35 | +```yaml |
| 36 | +canisters: |
| 37 | + - name: frontend |
| 38 | + recipe: |
| 39 | + type: "@dfinity/asset-canister@v2.1.0" |
| 40 | + configuration: |
| 41 | + build: |
| 42 | + - npm install |
| 43 | + - npm run generate --prefix app |
| 44 | + - npm run build |
| 45 | + dir: app/dist |
| 46 | +``` |
| 47 | +
|
| 48 | +The `build` array runs before the asset canister uploads files. `npm run generate` regenerates TypeScript bindings from the backend `.did` file; `npm run build` runs Vite. |
| 49 | + |
| 50 | +### vite.config.ts |
| 51 | + |
| 52 | +```typescript |
| 53 | +import { defineConfig } from "vite"; |
| 54 | +import react from "@vitejs/plugin-react"; |
| 55 | +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; |
| 56 | +
|
| 57 | +// Change these values to match your local replica. |
| 58 | +// The `icp network start` command prints the root key; |
| 59 | +// the `icp deploy` command prints the backend canister ID. |
| 60 | +const IC_ROOT_KEY_HEX = "<IC_ROOT_KEY_HEX>"; |
| 61 | +const BACKEND_CANISTER_ID = "<BACKEND_CANISTER_ID>"; |
| 62 | + |
| 63 | +export default defineConfig({ |
| 64 | + plugins: [ |
| 65 | + react(), |
| 66 | + icpBindgen({ |
| 67 | + didFile: "../../backend/backend.did", |
| 68 | + outDir: "./src/backend/api", |
| 69 | + }), |
| 70 | + ], |
| 71 | + server: { |
| 72 | + headers: { |
| 73 | + // Simulate the ic_env cookie that the asset canister injects in production. |
| 74 | + "Set-Cookie": `ic_env=${encodeURIComponent( |
| 75 | + `ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}` |
| 76 | + )}; SameSite=Lax;`, |
| 77 | + }, |
| 78 | + proxy: { |
| 79 | + "/api": { |
| 80 | + target: "http://127.0.0.1:8000", |
| 81 | + changeOrigin: true, |
| 82 | + }, |
| 83 | + }, |
| 84 | + }, |
| 85 | +}); |
| 86 | +``` |
| 87 | + |
| 88 | +The `icpBindgen` Vite plugin regenerates TypeScript bindings whenever the `.did` file changes during development. |
| 89 | + |
| 90 | +The `server.headers` block simulates the `ic_env` cookie during `vite dev`. In production, the asset canister injects this cookie automatically — your code reads it without any build-time environment variables. |
| 91 | + |
| 92 | +Install the required packages: |
| 93 | + |
| 94 | +```bash |
| 95 | +npm install @icp-sdk/core |
| 96 | +npm install -D @icp-sdk/bindgen @vitejs/plugin-react |
| 97 | +``` |
| 98 | + |
| 99 | +### Reading canister IDs and the root key |
| 100 | + |
| 101 | +```typescript |
| 102 | +import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env"; |
| 103 | +import { createActor } from "./backend/api/backend"; |
| 104 | + |
| 105 | +interface CanisterEnv { |
| 106 | + readonly "PUBLIC_CANISTER_ID:backend": string; |
| 107 | +} |
| 108 | + |
| 109 | +// Reads from the ic_env cookie injected by the asset canister (production) |
| 110 | +// or the Set-Cookie header set in vite.config.ts (development). |
| 111 | +const canisterEnv = getCanisterEnv<CanisterEnv>(); |
| 112 | +const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"]; |
| 113 | + |
| 114 | +const actor = createActor(canisterId, { |
| 115 | + agentOptions: { |
| 116 | + // In production, use the root key from the ic_env cookie. |
| 117 | + // In development (import.meta.env.DEV), fetch it from the local replica. |
| 118 | + rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined, |
| 119 | + shouldFetchRootKey: import.meta.env.DEV, |
| 120 | + }, |
| 121 | +}); |
| 122 | +``` |
| 123 | + |
| 124 | +The `createActor` function is generated by `@icp-sdk/bindgen` from your `.did` file. It returns a fully typed actor. See the [JS SDK docs](https://js.icp.build) for the full `HttpAgent` and `Actor` API. |
| 125 | + |
| 126 | +### SPA routing |
| 127 | + |
| 128 | +React apps use client-side routing. Without a fallback, refreshing on `/about` returns a 404 from the asset canister. Add a `.ic-assets.json5` file to your `public/` directory so it ends up in `dist/`: |
| 129 | + |
| 130 | +```json5 |
| 131 | +[ |
| 132 | + { |
| 133 | + // Apply security policy to all paths. Two separate rules are needed because |
| 134 | + // `security_policy` and `enable_aliasing` interact: the aliasing rule must |
| 135 | + // be evaluated last so it only applies to paths with no matching file. |
| 136 | + "match": "**/*", |
| 137 | + "security_policy": "standard", |
| 138 | + "allow_raw_access": false |
| 139 | + }, |
| 140 | + { |
| 141 | + // SPA fallback: serve index.html for any path that has no matching file. |
| 142 | + "match": "**/*", |
| 143 | + "enable_aliasing": true |
| 144 | + } |
| 145 | +] |
| 146 | +``` |
| 147 | + |
| 148 | +See [asset canister configuration](asset-canister.md) for the full `.ic-assets.json5` reference. |
| 149 | + |
| 150 | +## Vue with Vite |
| 151 | + |
| 152 | +Vue and Vite follow the same pattern as React. The only difference is the Vite plugin: |
| 153 | + |
| 154 | +```bash |
| 155 | +npm install @icp-sdk/core |
| 156 | +npm install -D @icp-sdk/bindgen @vitejs/plugin-vue |
| 157 | +``` |
| 158 | + |
| 159 | +```typescript |
| 160 | +// vite.config.ts |
| 161 | +import { defineConfig } from "vite"; |
| 162 | +import vue from "@vitejs/plugin-vue"; |
| 163 | +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; |
| 164 | + |
| 165 | +export default defineConfig({ |
| 166 | + plugins: [ |
| 167 | + vue(), |
| 168 | + icpBindgen({ |
| 169 | + didFile: "../backend/backend.did", |
| 170 | + outDir: "./src/backend/api", |
| 171 | + }), |
| 172 | + ], |
| 173 | + server: { |
| 174 | + proxy: { |
| 175 | + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, |
| 176 | + }, |
| 177 | + }, |
| 178 | +}); |
| 179 | +``` |
| 180 | + |
| 181 | +If your Vue app calls `getCanisterEnv()` to read canister IDs, add the same `server.headers` block from the React section to simulate the `ic_env` cookie during local development — otherwise `getCanisterEnv()` will throw because the cookie is absent. The `icp.yaml` configuration is the same as the React example — point `dir` at `dist`. |
| 182 | + |
| 183 | +## Authentication |
| 184 | + |
| 185 | +Authentication with Internet Identity is framework-agnostic — the `@icp-sdk/auth` package works the same way in React, Vue, Svelte, and Next.js static export mode. See the [Internet Identity guide](../authentication/internet-identity.md) for integration steps. |
| 186 | + |
| 187 | +## Svelte and SvelteKit |
| 188 | + |
| 189 | +For SvelteKit, you must configure static export mode before deploying — the asset canister serves static files and does not support server-side rendering. |
| 190 | + |
| 191 | +### SvelteKit with static adapter |
| 192 | + |
| 193 | +```bash |
| 194 | +npm install -D @sveltejs/adapter-static |
| 195 | +``` |
| 196 | + |
| 197 | +```javascript |
| 198 | +// svelte.config.js |
| 199 | +import adapter from "@sveltejs/adapter-static"; |
| 200 | + |
| 201 | +export default { |
| 202 | + kit: { |
| 203 | + adapter: adapter({ |
| 204 | + pages: "build", |
| 205 | + assets: "build", |
| 206 | + fallback: "index.html", // enables SPA mode |
| 207 | + }), |
| 208 | + }, |
| 209 | +}; |
| 210 | +``` |
| 211 | + |
| 212 | +```yaml |
| 213 | +# icp.yaml |
| 214 | +canisters: |
| 215 | + - name: frontend |
| 216 | + recipe: |
| 217 | + type: "@dfinity/asset-canister@v2.1.0" |
| 218 | + configuration: |
| 219 | + build: |
| 220 | + - npm install |
| 221 | + - npm run build |
| 222 | + dir: build |
| 223 | +``` |
| 224 | +
|
| 225 | +For Svelte (without SvelteKit), Vite is the standard build tool. The `vite.config.js` setup is the same as Vue — swap `@vitejs/plugin-vue` for `@sveltejs/vite-plugin-svelte`. |
| 226 | + |
| 227 | +## Next.js |
| 228 | + |
| 229 | +Next.js requires static export mode. Server components, API routes, and `getServerSideProps` are not supported in an asset canister — the canister only serves static files. |
| 230 | + |
| 231 | +Enable static export in your Next.js config: |
| 232 | + |
| 233 | +```javascript |
| 234 | +// next.config.js |
| 235 | +const nextConfig = { |
| 236 | + output: "export", |
| 237 | +}; |
| 238 | +
|
| 239 | +module.exports = nextConfig; |
| 240 | +``` |
| 241 | + |
| 242 | +This outputs static files to the `out/` directory. |
| 243 | + |
| 244 | +```yaml |
| 245 | +# icp.yaml |
| 246 | +canisters: |
| 247 | + - name: frontend |
| 248 | + recipe: |
| 249 | + type: "@dfinity/asset-canister@v2.1.0" |
| 250 | + configuration: |
| 251 | + build: |
| 252 | + - npm install |
| 253 | + - npm run build |
| 254 | + dir: out |
| 255 | +``` |
| 256 | + |
| 257 | +:::note |
| 258 | +Only Next.js pages that can be statically generated are compatible with ICP. Any page using dynamic server-side features (server actions, route handlers, middleware) will not work in a static export. |
| 259 | +::: |
| 260 | + |
| 261 | +## Game engines |
| 262 | + |
| 263 | +Game engines that export HTML5 or WebGL builds can be deployed as asset canisters without a backend canister. The build output is pre-generated in the export step of the engine — `icp.yaml` just copies the files into place. |
| 264 | + |
| 265 | +### Unity WebGL |
| 266 | + |
| 267 | +Export your game from Unity Editor: **File → Build Settings → WebGL → Build**. This creates a folder with `index.html`, `Build/`, and `TemplateData/`. |
| 268 | + |
| 269 | +```yaml |
| 270 | +# icp.yaml |
| 271 | +canisters: |
| 272 | + - name: unity_webgl_template_assets |
| 273 | + recipe: |
| 274 | + type: "@dfinity/asset-canister@v2.1.0" |
| 275 | + configuration: |
| 276 | + dir: dist |
| 277 | + build: |
| 278 | + - mkdir -p dist |
| 279 | + - cp -r src/unity_webgl_template_assets/assets/* dist/ |
| 280 | + - cp -r src/unity_webgl_template_assets/src/* dist/ |
| 281 | +``` |
| 282 | + |
| 283 | +The `build` commands copy the Unity WebGL export into `dist/`. Point `dir` at that directory. |
| 284 | + |
| 285 | +See the [Unity WebGL example](https://github.com/dfinity/examples/tree/master/hosting/unity-webgl-template) for the full project structure. |
| 286 | + |
| 287 | +### Godot HTML5 |
| 288 | + |
| 289 | +Export your game from Godot Editor: **Project → Export → HTML5 → Export Project**. This creates an `index.html` and supporting files. |
| 290 | + |
| 291 | +```yaml |
| 292 | +# icp.yaml |
| 293 | +canisters: |
| 294 | + - name: godot_html5_assets |
| 295 | + recipe: |
| 296 | + type: "@dfinity/asset-canister@v2.1.0" |
| 297 | + configuration: |
| 298 | + dir: dist |
| 299 | + build: |
| 300 | + - mkdir -p dist |
| 301 | + - cp -r src/godot_html5_assets/assets/* dist/ |
| 302 | + - cp -r src/godot_html5_assets/src/* dist/ |
| 303 | +``` |
| 304 | + |
| 305 | +See the [Godot HTML5 example](https://github.com/dfinity/examples/tree/master/hosting/godot-html5-template) for the full project structure. |
| 306 | + |
| 307 | +### Deploying game builds |
| 308 | + |
| 309 | +Both game engine templates deploy with standard icp-cli commands: |
| 310 | + |
| 311 | +```bash |
| 312 | +# Start local network |
| 313 | +icp network start -d |
| 314 | +
|
| 315 | +# Deploy the asset canister |
| 316 | +icp deploy |
| 317 | +
|
| 318 | +# Access your game locally |
| 319 | +# http://<canister-id>.localhost:8000 |
| 320 | +``` |
| 321 | + |
| 322 | +No Vite plugin or JS SDK integration is needed for game builds — the asset canister serves the pre-built HTML and JavaScript files directly. |
| 323 | + |
| 324 | +## Static sites |
| 325 | + |
| 326 | +For sites with no backend canister (portfolios, landing pages, documentation): |
| 327 | + |
| 328 | +```yaml |
| 329 | +# icp.yaml |
| 330 | +canisters: |
| 331 | + - name: frontend |
| 332 | + recipe: |
| 333 | + type: "@dfinity/asset-canister@v2.1.0" |
| 334 | + configuration: |
| 335 | + build: |
| 336 | + - npm install |
| 337 | + - npm run build |
| 338 | + dir: dist |
| 339 | +``` |
| 340 | + |
| 341 | +No JS SDK integration is needed. The asset canister serves your files, and you can configure headers and caching in `.ic-assets.json5`. |
| 342 | + |
| 343 | +See the [React hosting example](https://github.com/dfinity/examples/tree/master/hosting/react) for a minimal static frontend without a backend canister. |
| 344 | + |
| 345 | +## Deploy |
| 346 | + |
| 347 | +```bash |
| 348 | +# Start local network |
| 349 | +icp network start -d |
| 350 | +
|
| 351 | +# Deploy all canisters |
| 352 | +icp deploy |
| 353 | +
|
| 354 | +# Deploy to mainnet |
| 355 | +icp deploy -e ic |
| 356 | +``` |
| 357 | + |
| 358 | +After deployment, the asset canister URL depends on your canister ID: |
| 359 | + |
| 360 | +| Environment | URL | |
| 361 | +|-------------|-----| |
| 362 | +| Local | `http://<canister-id>.localhost:8000` | |
| 363 | +| Mainnet | `https://<canister-id>.ic0.app` | |
| 364 | + |
| 365 | +Get your canister ID with: |
| 366 | + |
| 367 | +```bash |
| 368 | +icp canister settings show frontend -i |
| 369 | +``` |
| 370 | + |
| 371 | +## Next steps |
| 372 | + |
| 373 | +- [Asset canister](asset-canister.md) — configure headers, caching, and SPA routing in `.ic-assets.json5` |
| 374 | +- [Internet Identity](../authentication/internet-identity.md) — add authentication to your frontend |
| 375 | +- [Project structure](../../getting-started/project-structure.md) — explore the hello-world template with React, Vite, and a Motoko backend |
| 376 | + |
| 377 | +<!-- Upstream: informed by dfinity/icskills — skills/asset-canister/SKILL.md; dfinity/icp-cli-templates — hello-world/frontend/; dfinity/examples — hosting/react, hosting/unity-webgl-template, hosting/godot-html5-template, svelte/svelte-motoko-starter; dfinity/icp-js-sdk-docs — core/canister-environment.mdx, bindgen/plugins/vite/index.md, auth/quick-start.md --> |
0 commit comments