Skip to content

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

  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

ProjectPublish this folderBuild
React, Vue or Svelte with Vitedistnpm run build
Create React Appbuildnpm run build
Angulardist/<project>/browserng build
Next.jsoutnext build with output: 'export'
Nuxt.output/publicnpx nuxt generate
SvelteKitbuildnpm run build with adapter-static
Astrodistnpm run build
Plain HTML, CSS and JavaScriptthe folder, or the single filenothing 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 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 happensWhat to do
Needs a server at runtime: Next.js server rendering, API routes, Server Actions, Remix or SvelteKit server code, PHP, a databaseNone of it runs, because there is no serverExport 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 WebSocketThe sandbox blocks every request except to the prototype's own origin, so the prototype shows its empty or error stateUse 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 mapBlocked, alwaysBundle the script into the build and drop the embed
Loads images, fonts, stylesheets or media from other sitesBlocked by defaultBundle 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.openNothing opens, and window.open returns nullLink 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: linkThe 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 secondsNothing. 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 reloadsUse an <a href> link for the destination, or show the next state in the page
Calls alert, confirm or promptNo dialog appears. alert returns at once, confirm returns false and prompt returns null, so a flow behind if (confirm(...)) always takes the cancel branchUse an in-page modal, such as the <dialog> element
Submits a formThe 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 pageHandle submit in JavaScript with event.preventDefault() and show the next state
Uses cookies, indexedDB, a service worker or Cache Storagedocument.cookie throws a SecurityError on read and write, indexedDB.open fails with the same error, and navigator.serviceWorker and caches throw it when readKeep 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.