From 6a2ae1b9476160b5f717a40fb913a4e967c1427d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 18:14:04 +0200 Subject: [PATCH 1/2] docs: frontend frameworks guide --- docs/guides/frontends/frameworks.md | 368 +++++++++++++++++++++++++++- 1 file changed, 356 insertions(+), 12 deletions(-) diff --git a/docs/guides/frontends/frameworks.md b/docs/guides/frontends/frameworks.md index ace39e59..ec42449f 100644 --- a/docs/guides/frontends/frameworks.md +++ b/docs/guides/frontends/frameworks.md @@ -1,21 +1,365 @@ --- title: "Frontend Frameworks" -description: "Integrate React, Svelte, Vue, Unity, and other frameworks with ICP canisters" +description: "Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using the asset canister and icp-cli" sidebar: order: 4 --- -TODO: Write content for this page. +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. - -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. +This guide shows you how to configure your framework's build pipeline, wire up the ICP JavaScript SDK, and deploy to an asset canister. - -- Portal: building-apps/frontends/existing-frontend.mdx -- JS SDK: @icp-sdk/core (https://js.icp.build/core), @icp-sdk/auth (https://js.icp.build/auth) -- Examples: hosting/react, svelte/svelte-motoko-starter, svelte/sveltekit-starter, hosting/godot-html5-template, hosting/unity-webgl-template +## Prerequisites - -- guides/frontends/asset-canister -- deployment target -- guides/authentication/internet-identity -- auth setup per framework -- getting-started/project-structure -- Vite plugin in hello-world template +- [icp-cli](https://cli.internetcomputer.org/) installed: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- A backend canister deployed (or a static-only site with no backend) +- Familiarity with [asset canisters](asset-canister.md) + +## The deployment model + +Every frontend framework integration follows the same pattern: + +1. Configure `icp.yaml` to point at your framework's build output directory +2. Optionally add a Vite plugin (`@icp-sdk/bindgen`) to generate typed canister bindings at build time +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 +4. Deploy with `icp deploy` + +The asset canister injects an `ic_env` cookie into every HTML response. This cookie carries the root key and any `PUBLIC_CANISTER_ID:` environment variables you set — so your frontend never needs canister IDs baked into the build artifact. + +## React with Vite + +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. + +### icp.yaml + +```yaml +canisters: + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + build: + - npm install + - npm run generate --prefix app + - npm run build + dir: app/dist +``` + +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. + +### vite.config.ts + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; + +export default defineConfig({ + plugins: [ + react(), + icpBindgen({ + didFile: "../../backend/backend.did", + outDir: "./src/backend/api", + }), + ], + server: { + headers: { + // Simulate the ic_env cookie that the asset canister injects in production. + // Replace IC_ROOT_KEY_HEX and BACKEND_CANISTER_ID with values from your + // local replica after running `icp network start -d` and `icp deploy`. + "Set-Cookie": `ic_env=${encodeURIComponent( + "ic_root_key=&PUBLIC_CANISTER_ID:backend=" + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { + target: "http://127.0.0.1:8000", + changeOrigin: true, + }, + }, + }, +}); +``` + +The `icpBindgen` Vite plugin regenerates TypeScript bindings whenever the `.did` file changes during development. + +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. + +Install the required packages: + +```bash +npm install @icp-sdk/core +npm install -D @icp-sdk/bindgen @vitejs/plugin-react +``` + +### Reading canister IDs and the root key + +```typescript +import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env"; +import { createActor } from "./backend/api/backend"; + +interface CanisterEnv { + readonly "PUBLIC_CANISTER_ID:backend": string; +} + +// Reads from the ic_env cookie injected by the asset canister (production) +// or the Set-Cookie header set in vite.config.ts (development). +const canisterEnv = getCanisterEnv(); +const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"]; + +const actor = createActor(canisterId, { + agentOptions: { + // In production, use the root key from the ic_env cookie. + // In development (import.meta.env.DEV), fetch it from the local replica. + rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined, + shouldFetchRootKey: import.meta.env.DEV, + }, +}); +``` + +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. + +### SPA routing + +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/`: + +```json5 +[ + { + "match": "**/*", + "security_policy": "standard", + "allow_raw_access": false + }, + { + "match": "**/*", + "enable_aliasing": true + } +] +``` + +See [asset canister configuration](asset-canister.md) for the full `.ic-assets.json5` reference. + +## Vue with Vite + +Vue and Vite follow the same pattern as React. The only difference is the Vite plugin: + +```bash +npm install @icp-sdk/core +npm install -D @icp-sdk/bindgen @vitejs/plugin-vue +``` + +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; + +export default defineConfig({ + plugins: [ + vue(), + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/backend/api", + }), + ], + server: { + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }, +}); +``` + +The `icp.yaml` configuration is the same as the React example — point `dir` at `dist`. + +## Svelte and SvelteKit + +For SvelteKit, you must configure static export mode before deploying — the asset canister serves static files and does not support server-side rendering. + +### SvelteKit with static adapter + +```bash +npm install -D @sveltejs/adapter-static +``` + +```javascript +// svelte.config.js +import adapter from "@sveltejs/adapter-static"; + +export default { + kit: { + adapter: adapter({ + pages: "build", + assets: "build", + fallback: "index.html", // enables SPA mode + }), + }, +}; +``` + +```yaml +# icp.yaml +canisters: + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + build: + - npm install + - npm run build + dir: build +``` + +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`. + +## Next.js + +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. + +Enable static export in your Next.js config: + +```javascript +// next.config.js +const nextConfig = { + output: "export", +}; + +module.exports = nextConfig; +``` + +This outputs static files to the `out/` directory. + +```yaml +# icp.yaml +canisters: + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + build: + - npm install + - npm run build + dir: out +``` + +:::note +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. +::: + +## Game engines + +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. + +### Unity WebGL + +Export your game from Unity Editor: **File → Build Settings → WebGL → Build**. This creates a folder with `index.html`, `Build/`, and `TemplateData/`. + +```yaml +# icp.yaml +canisters: + - name: unity_webgl_template_assets + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: dist + build: + - mkdir -p dist + - cp -r src/unity_webgl_template_assets/assets/* dist/ + - cp -r src/unity_webgl_template_assets/src/* dist/ +``` + +The `build` commands copy the Unity WebGL export into `dist/`. Point `dir` at that directory. + +See the [Unity WebGL example](https://github.com/dfinity/examples/tree/master/hosting/unity-webgl-template) for the full project structure. + +### Godot HTML5 + +Export your game from Godot Editor: **Project → Export → HTML5 → Export Project**. This creates an `index.html` and supporting files. + +```yaml +# icp.yaml +canisters: + - name: godot_html5_assets + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: dist + build: + - mkdir -p dist + - cp -r src/godot_html5_assets/assets/* dist/ + - cp -r src/godot_html5_assets/src/* dist/ +``` + +See the [Godot HTML5 example](https://github.com/dfinity/examples/tree/master/hosting/godot-html5-template) for the full project structure. + +### Deploying game builds + +Both game engine templates deploy with standard icp-cli commands: + +```bash +# Start local network +icp network start -d + +# Deploy the asset canister +icp deploy + +# Access your game locally +# http://.localhost:8000 +``` + +No Vite plugin or JS SDK integration is needed for game builds — the asset canister serves the pre-built HTML and JavaScript files directly. + +## Static sites + +For sites with no backend canister (portfolios, landing pages, documentation): + +```yaml +# icp.yaml +canisters: + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + build: + - npm install + - npm run build + dir: dist +``` + +No JS SDK integration is needed. The asset canister serves your files, and you can configure headers and caching in `.ic-assets.json5`. + +See the [React hosting example](https://github.com/dfinity/examples/tree/master/hosting/react) for a minimal static frontend without a backend canister. + +## Deploy + +```bash +# Start local network +icp network start -d + +# Deploy all canisters +icp deploy + +# Deploy to mainnet +icp deploy -e ic +``` + +After deployment, the asset canister URL depends on your canister ID: + +| Environment | URL | +|-------------|-----| +| Local | `http://.localhost:8000` | +| Mainnet | `https://.ic0.app` | + +Get your canister ID with: + +```bash +icp canister id frontend +``` + +## Next steps + +- [Asset canister](asset-canister.md) — configure headers, caching, and SPA routing in `.ic-assets.json5` +- [Internet Identity](../authentication/internet-identity.md) — add authentication to your frontend +- [Project structure](../../getting-started/project-structure.md) — explore the hello-world template with React, Vite, and a Motoko backend + + From 06169620c57756cb32e322e2a3798174a490d670 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 20:47:52 +0200 Subject: [PATCH 2/2] fix(frontends/frameworks): address review feedback - Fix icp canister id command: replace non-existent 'icp canister id frontend' with correct 'icp canister settings show frontend -i' - Adopt named-constants pattern for vite.config.ts (matches hello-world template) - Add note to Vue section about server.headers ic_env cookie simulation in dev - Add Authentication section noting @icp-sdk/auth is framework-agnostic - Add comments to SPA .ic-assets.json5 explaining the two-rule structure --- docs/guides/frontends/frameworks.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/guides/frontends/frameworks.md b/docs/guides/frontends/frameworks.md index ec42449f..a93374da 100644 --- a/docs/guides/frontends/frameworks.md +++ b/docs/guides/frontends/frameworks.md @@ -54,6 +54,12 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; +// Change these values to match your local replica. +// The `icp network start` command prints the root key; +// the `icp deploy` command prints the backend canister ID. +const IC_ROOT_KEY_HEX = ""; +const BACKEND_CANISTER_ID = ""; + export default defineConfig({ plugins: [ react(), @@ -65,10 +71,8 @@ export default defineConfig({ server: { headers: { // Simulate the ic_env cookie that the asset canister injects in production. - // Replace IC_ROOT_KEY_HEX and BACKEND_CANISTER_ID with values from your - // local replica after running `icp network start -d` and `icp deploy`. "Set-Cookie": `ic_env=${encodeURIComponent( - "ic_root_key=&PUBLIC_CANISTER_ID:backend=" + `ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}` )}; SameSite=Lax;`, }, proxy: { @@ -126,11 +130,15 @@ React apps use client-side routing. Without a fallback, refreshing on `/about` r ```json5 [ { + // Apply security policy to all paths. Two separate rules are needed because + // `security_policy` and `enable_aliasing` interact: the aliasing rule must + // be evaluated last so it only applies to paths with no matching file. "match": "**/*", "security_policy": "standard", "allow_raw_access": false }, { + // SPA fallback: serve index.html for any path that has no matching file. "match": "**/*", "enable_aliasing": true } @@ -170,7 +178,11 @@ export default defineConfig({ }); ``` -The `icp.yaml` configuration is the same as the React example — point `dir` at `dist`. +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`. + +## Authentication + +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. ## Svelte and SvelteKit @@ -353,7 +365,7 @@ After deployment, the asset canister URL depends on your canister ID: Get your canister ID with: ```bash -icp canister id frontend +icp canister settings show frontend -i ``` ## Next steps