@nominalcrew/webflow-kit
v0.5.0
Published
Vite plugin and CLI for Webflow sites: local HTTP dev, staging JS+CSS sync on save, and R2 deploys.
Maintainers
Readme
@nominalcrew/webflow-kit
Vite plugin and CLI for Webflow site repos. Internal agency tooling, published publicly so projects can install it — the license does not grant third-party use.
It does not ship a CDN origin. Each site passes its own cdn (or PUBLIC_ASSET_URL in .env).
- Dev: HTTP Vite on
http://localhost:3000, then open the site’s Webflow staging URL with?nc-env=devso the site loader pulls local modules. JS and CSS are also rebuilt and uploaded to{name}/staging/on start and on every save. - Deploy: upload every file in
dist/to R2 under{name}/staging/or{name}/production/. Restrict extensions withupload.include.
This directory is the package root. Depend on it from each site; do not copy it into a site as source.
What a site gets
| Command in the site repo | What the kit does |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| pnpm dev (vite) | HTTP dev server, CORS, CSS HMR, full reload on JS, open WEBFLOW_STAGING_URL?nc-env=dev, upload the staging build on save |
| pnpm deploy:staging | Build unminified + sourcemaps, upload every file in dist/ to {name}/staging/ |
| pnpm deploy:production | Build minified, upload every file in dist/ to {name}/production/ |
pnpm dev uploads the build output to staging only (never production). Production still requires deploy:production. Disable the save-sync with WEBFLOW_SYNC_STAGING=false.
Add it to a site
pnpm add -D @nominalcrew/webflow-kitNode.js 24+. Peer: Vite 8.
To work on the kit itself next to a site:
{
"devDependencies": {
"@nominalcrew/webflow-kit": "link:../webflow-kit"
}
}1. Site vite.config.js
Load .env before the plugin so process.env is filled. Secrets and the CDN origin stay in the site .env, never in this package.
import "dotenv/config";
import { defineConfig } from "vite";
import { webflowKit } from "@nominalcrew/webflow-kit";
import config from "./webflow.config.js";
export default defineConfig(({ mode }) => ({
plugins: [
webflowKit({
mode,
...config,
cdn: process.env.PUBLIC_ASSET_URL,
}),
],
}));cdn is the public origin of your asset bucket (no trailing slash), for example https://cdn.example.com. After a deploy, the CLI prints {cdn}/{name}/staging/bundle.js.
You can also set cdn in webflow.config.js. The value passed to webflowKit() wins; if both are omitted, PUBLIC_ASSET_URL is read from the environment.
2. Site webflow.config.js
name is the R2 folder and must match the loader data-project.
export default {
name: "my-site",
assets: {
js: "bundle.js",
css: "bundle.css",
},
environments: {
staging: { path: "staging" },
production: { path: "production" },
},
};3. Site .env
Copy .env.example from this repo into the site.
| Variable | Required | Role |
| ---------------------- | ---------------------- | -------------------------------------------------------------------- |
| PUBLIC_ASSET_URL | for public deploy URLs | CDN origin passed as cdn |
| WEBFLOW_STAGING_URL | for pnpm dev | Published *.webflow.io URL; opened with ?nc-env=dev |
| WEBFLOW_SYNC_STAGING | no | Default true. Set false to skip JS+CSS uploads during pnpm dev |
| R2_ACCOUNT_ID | for deploy | Cloudflare account id |
| R2_ACCESS_KEY_ID | for deploy | R2 access key |
| R2_SECRET_ACCESS_KEY | for deploy | R2 secret |
| R2_BUCKET | for deploy | Bucket name |
| PROJECT_ID | no | Overrides name for the R2 prefix |
| R2_STAGING_PREFIX | no | Defaults to staging |
| R2_PRODUCTION_PREFIX | no | Defaults to production |
Shared across sites on the same account: R2 keys, PUBLIC_ASSET_URL. Per site: name and WEBFLOW_STAGING_URL.
If WEBFLOW_STAGING_URL is empty, Vite still starts and logs a warning. It does not open a browser.
4. Webflow
Site Settings → Custom Code → Head. Staging CSS first (static <link>, so the Designer canvas can load it), then the loader. Do not add bundle.js. Host the loader on your CDN. data-project must equal name:
<link
rel="stylesheet"
href="https://cdn.example.com/my-site/staging/bundle.css"
/>
<script src="https://cdn.example.com/loader.js" data-project="my-site"></script>The <link> must come before the script. Publish the site once so .webflow.io exists, then put that URL in WEBFLOW_STAGING_URL.
5. Site package.json scripts
{
"scripts": {
"dev": "vite",
"build:staging": "vite build --mode staging",
"build:production": "vite build --mode production",
"deploy:staging": "pnpm build:staging && webflow-kit deploy staging",
"deploy:production": "pnpm build:production && webflow-kit deploy production"
}
}Daily loop (from the site repo)
pnpm dev- Vite listens on
http://localhost:3000. - The browser opens
{WEBFLOW_STAGING_URL}?nc-env=dev. - The loader injects
@vite/clientand/src/js/main.js. - Staging JS + CSS are uploaded (
{name}/staging/bundle.*). - Save CSS → HMR on the published tab, and staging is uploaded again. Refresh the Designer to see CSS in the canvas.
- Save JS → full page reload on the published tab, and staging JS + CSS are uploaded.
.webflow.iowithout?nc-env=devthen has the last save.
First run: if scripts fail, confirm Vite is running, then reload the Webflow tab.
pnpm deploy:staging # QA / client on *.webflow.io (no ?nc-env=dev)
pnpm deploy:production # live custom domainR2 keys:
{name}/staging/bundle.js
{name}/staging/bundle.css
{name}/staging/assets/…
{name}/production/bundle.js
{name}/production/bundle.css
{name}/production/assets/…Every file written to dist/ is uploaded, keeping its relative path. Omit upload.include to send all of them. Set it to allow only some extensions:
upload: {
include: ["js", "css", "svg", "png", "webp", "woff2"],
}Code-split chunks stay next to the entry on the CDN (./assets/… from bundle.js). Override the pattern with assets.chunks.
CLI (same thing, from the site cwd after a build):
webflow-kit deploy staging
webflow-kit deploy productionPlugin options
| Option | Default | Description |
| ------------------------------ | ------------------------- | ------------------------------------- |
| cdn | PUBLIC_ASSET_URL | Public CDN origin (no trailing slash) |
| mode | from Vite | staging or production |
| name | PROJECT_ID or project | R2 folder |
| entry | ./src/js/main.js | JS entry |
| outDir | dist | Build output |
| assets.js | bundle.js | Uploaded JS filename |
| assets.css | bundle.css | Uploaded CSS filename |
| assets.chunks | assets/[name]-[hash].js | Code-split chunk filenames |
| upload.include | all files | Allowed extensions, without the dot |
| environments.staging.path | staging | Folder under {name}/ |
| environments.production.path | production | Folder under {name}/ |
| server | HTTP localhost:3000 | Vite server overrides |
Default server: host: true, strictPort: true, cors: true, HMR on ws://localhost:3000. Override via server in webflow.config.js if a site cannot use port 3000 — then set the loader data-dev-origin to match.
Publish
The package is on npm: @nominalcrew/webflow-kit. License: UNLICENSED (agency use only).
To release a new version, bump version in package.json, then:
npm login
pnpm publish --access public