Start here
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.
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
- Run your project's build, the same one you would run before deploying to any static host.
- Publish the folder it writes, the one that holds
index.html.
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: truenext tooutput: 'export'in your Next config, and follow Next's static export guide for images. - SvelteKit: set
export const prerender = trueandexport const trailingSlash = 'always'in the root layout. - Angular: a project still on the older browser builder writes to
dist/<project>, without thebrowserfolder. If you use server-side rendering, setoutputModetostaticinangular.jsonso 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.jsfrom abaseorhomepagesetting made for GitHub Pages, answers 404 and the page loads with no script or style. A relative base such as Vite'sbase: './'loads the first page, but a reload or a deep link on a client-side route such as/checkout/step-2looks for/checkout/assets/app.jsand 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
.htmlfile with inline styles and scripts works too. - Client-side routing. A route with no file extension, such as
/checkout/step-2, answers withindex.html, so React Router, Vue Router, the Angular router and similar work on a reload or a deep link. A path that starts withapi/or_/is not rewritten this way. - Root-absolute asset paths.
/assets/app.jsand/_astro/site.cssresolve, 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
fetchof 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 callshistory.pushState. - Browser storage that starts empty.
localStorageandsessionStoragework 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.indexedDBanswers everyopenwith an error, anddocument.cookiethrows on read and write, so keep state inlocalStorageor 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 |
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.
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 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 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. 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.