# What you can publish

> React, Angular, Vue and other frameworks work as static builds. critlab hosts the output and does not build it. Build folders, what works and what does not.

Source: https://critlab.app/docs/what-you-can-publish

critlab hosts the files your build produces. It does not install packages, run a build or run a server, so a React, Angular, Vue, Svelte or Astro project works once you have built it into static files. You, or your coding agent, run the project's own build and publish the output folder.

## Build first, then publish

1. Run your project's build, the same one you would run before deploying to any static host.
2. Publish the folder it writes, the one that holds `index.html`.

```bash
npm run build
crit publish ./dist --name "Checkout redesign"
```

You can also drop that folder, a zip of it, or a single `.html` file on **New prototype** in the app, or let your agent call `critlab_publish` with `{ dir }`. Publishing again to the same prototype adds a version on the same link.

Publish the build output, not the project root. Source code in `src/` or a `package.json` does not run on critlab, and a folder without an `index.html` is refused before anything is uploaded. `node_modules`, `.git`, `.env` files and other secrets are never zipped, and the result tells you what was left out.

Source maps are served to reviewers only when **Who can see code** is set to Everyone with access, and every other file in the package can be fetched by anyone who can open the prototype.

## The build folder for each framework

| Project | Publish this folder | Build |
| --- | --- | --- |
| React, Vue or Svelte with Vite | `dist` | `npm run build` |
| Create React App | `build` | `npm run build` |
| Angular | `dist/<project>/browser` | `ng build` |
| Next.js | `out` | `next build` with `output: 'export'` |
| Nuxt | `.output/public` | `npx nuxt generate` |
| SvelteKit | `build` | `npm run build` with `adapter-static` |
| Astro | `dist` | `npm run build` |
| Plain HTML, CSS and JavaScript | the folder, or the single file | nothing to build |

A few settings make a multi-page export work. critlab serves `/about/` from `about/index.html`, but it does not turn `/about` into `about.html`, so ask the framework for folder-style routes:

- **Next.js:** set `trailingSlash: true` next to `output: 'export'` in your Next config, and follow Next's [static export guide](https://nextjs.org/docs/app/guides/static-exports) for images.
- **SvelteKit:** set `export const prerender = true` and `export const trailingSlash = 'always'` in the root layout.
- **Angular:** a project still on the older browser builder writes to `dist/<project>`, without the `browser` folder. If you use server-side rendering, set `outputMode` to `static` in `angular.json` so the build is plain files with no server.
- **Astro:** the default output is already folder-style.
- **Any framework:** leave the base path at its default. critlab serves each version from its root, so an asset path with a prefix, such as `/my-repo/assets/app.js` from a `base` or `homepage` setting made for GitHub Pages, answers 404 and the page loads with no script or style. A relative base such as Vite's `base: './'` loads the first page, but a reload or a deep link on a client-side route such as `/checkout/step-2` looks for `/checkout/assets/app.js` and fails the same way.

## What works

- **Any framework that ends in static files.** React, Vue, Svelte, Angular, Astro, Next.js and Nuxt exports, or plain HTML. A single `.html` file with inline styles and scripts works too.
- **Client-side routing.** A route with no file extension, such as `/checkout/step-2`, answers with `index.html`, so React Router, Vue Router, the Angular router and similar work on a reload or a deep link. A path that starts with `api/` or `_/` is not rewritten this way.
- **Root-absolute asset paths.** `/assets/app.js` and `/_astro/site.css` resolve, because every version of a prototype has its own origin and the package is served from its root.
- **Files in the build.** Images, fonts, CSS, video, audio, JSON and web workers that ship in the package load normally, and so does a `fetch` of a file in the build, such as `/data/products.json`.
- **Links between pages.** A link to another page of the package, or to a `#` anchor, works as it does on any site, with one difference: a click on a link to another page replaces the current page instead of adding a history entry, so the browser's Back button leaves the review instead of stepping back through the prototype's pages. A link that only moves to a `#` anchor keeps its history entry, and so does a client-side router that calls `history.pushState`.
- **Browser storage that starts empty.** `localStorage` and `sessionStorage` work in memory. They are empty on every page load, including a link to another page of the prototype, and keep their values across client-side route changes. `indexedDB` answers every `open` with an error, and `document.cookie` throws on read and write, so keep state in `localStorage` or in the code.

## What does not work, and what to do instead

| If your prototype... | What happens | What to do |
| --- | --- | --- |
| Needs a server at runtime: Next.js server rendering, API routes, Server Actions, Remix or SvelteKit server code, PHP, a database | None of it runs, because there is no server | Export or prerender to static files first, or host it yourself and [connect it as an external project](/docs/connect-a-site) |
| Loads live data from your API or another site with `fetch`, XHR or a WebSocket | The sandbox blocks every request except to the prototype's own origin, so the prototype shows its empty or error state | Use mock data: a JSON file in the build, or fixtures in the code. A file such as `api/users.json` works, a bare `/api/users` route does not |
| Loads a script or an iframe from another site: a CDN script, analytics, an embedded YouTube video or map | Blocked, always | Bundle the script into the build and drop the embed |
| Loads images, fonts, stylesheets or media from other sites | Blocked by default | Bundle them, or switch on **Allow external images & fonts** for that prototype in Share, or run `crit access <slug> --external-assets on` |
| Opens a popup with `window.open` | Nothing opens, and `window.open` returns `null` | Link to a screen inside the prototype, or show the content in the page |
| Links to another site, with or without `target="_blank"`, or has a `mailto:` or `tel:` link | The click does not leave the prototype. The viewer asks "Leave the prototype?" in a dialog that names where the link goes, with **Stay** and **Open in new tab** (**Write email** or **Call** for `mailto:` and `tel:`). The tab opens only when the reviewer clicks **Open in new tab**, and after **Stay** the dialog stays quiet for ten seconds | Nothing. Use an ordinary `<a href>` link; `https://`, `http://`, `mailto:` and `tel:` links all work this way |
| Sets `location` to another site in script, such as `location.href = "https://..."` | Not caught: the prototype is replaced by the browser's error page until the reviewer reloads | Use an `<a href>` link for the destination, or show the next state in the page |
| Calls `alert`, `confirm` or `prompt` | No dialog appears. `alert` returns at once, `confirm` returns `false` and `prompt` returns `null`, so a flow behind `if (confirm(...))` always takes the cancel branch | Use an in-page modal, such as the `<dialog>` element |
| Submits a form | The browser blocks the request, whatever the method, and the `submit` event still fires. A form whose `action` is another site also replaces the prototype with the error page | Handle `submit` in JavaScript with `event.preventDefault()` and show the next state |
| Uses cookies, `indexedDB`, a service worker or Cache Storage | `document.cookie` throws a `SecurityError` on read and write, `indexedDB.open` fails with the same error, and `navigator.serviceWorker` and `caches` throw it when read | Keep state in `localStorage` or in the code, and switch off a service worker or PWA plugin in the review build |

When you publish, critlab warns you about the remote files your pages and stylesheets refer to, with the URLs and the fix. A URL that a script builds at runtime is not found by that check. Scripts and requests to other sites never load, whatever the switch says. The reasons are in the [security overview](/docs/enterprise/security).

To review a site you host, including one that needs a server or a database, connect it as an external project: one script tag brings critlab's comments to the live site, and the site sits in your folder like any prototype. [Connect a site you host](/docs/connect-a-site) walks through it. External projects are free while the beta runs and count toward the beta's prototype limit.

## Screens and states

For a prototype with more than one screen or any designed state, add a `critlab.canvas.json` next to `index.html`. Without it, critlab finds screens by following links in the page, and it never clicks, so a screen reached only by a button or a router call can be missed. [The canvas manifest](/docs/agents/canvas) lists what to write.

## Limits

A package needs an `index.html` at its root, or inside the single top-level folder of a zip, and may hold up to 5,000 files. The unpacked size limit is 100 MiB on the cloud while the free beta runs and on a Pro workspace, and 25 MiB on a Free workspace otherwise. A self-hosted instance sets both limits with `MAX_PACKAGE_BYTES_FREE` and `MAX_PACKAGE_BYTES_PRO`: see the [configuration reference](/docs/self-hosting/configuration). If a build is too large, strip source maps and compress images.

## Publishing with an agent

An agent follows the same two steps: it builds the project, then publishes the build output, never the source. Everything it needs is in the default tool group: [Choose your tools](/docs/agents/tools).
