Read SKILL.md first. First-time ship: shipping.md (GitHub MCP + Netlify + auth fallbacks). Failure modes: gotchas.md.
Catalog, don't touch:
- Top-level sections in
index.html→ map to route components or section components. - Font files in
fonts/→ note every weight/style file; check whichfont-familynames the CSS references. - Compiled CSS under
css/— typically one large site-specific file +normalize.css+webflow.css. js/folder:jquery*.js,webflow.js→ drop, replaced by React.gsap*,ScrollTrigger*,SplitText*, any custom GSAP code → keep, port to client-onlyuseEffect+gsap.context()(see §8).
- Analytics scripts in
<head>— catalog; replacement is user-dependent (none, Plausible, Fathom, keep GA via GTM, etc.). - Hidden sections in the export — if the ZIP does not include
w-hidden/hideon nodes that are hidden in the live Designer, see gotchas.md — you may need to add classes manually after comparing to production. - Dead HTML pages (
style-guide.html,401.html,404.html) — drop unless asked.
<repo-root>/
index.html css/ fonts/ images/ js/ # original Webflow export, read-only reference
package.json # proxy scripts only → web/
README.md netlify.toml
web/ # TanStack Start app
vite.config.ts # tanstackStart() + @netlify/vite-plugin-tanstack-start
package.json
public/
fonts/ images/ # copied from export; URLs in CSS → /fonts /images
src/
routes/
__root.tsx # root layout: global CSS, shell, head() meta/links, providers
index.tsx # home route (marketing page)
site/
seo.ts # optional: title, description, OG/Twitter from export index.html
...
styles/
marketing.css
site-fonts.css
marketing/ # compiled Webflow CSS split by concern
components/
HomeBody.tsx
MarketingSiteRoot.tsx
home/sections/...
hooks/
lib/
TanStack Start generates router.tsx, routeTree.gen.ts, etc. — keep the generator output; add Webflow files alongside.
Create at repo root — only proxy scripts. See templates/root-package.json.
From repo root (empty web/ or move scaffold after):
# Official CLI (package manager examples — use one)
pnpm dlx create-start-app@latest web
# or: npx create-start-app@latest webChoose React when prompted. cd web and verify pnpm dev / npm run dev works.
Per TanStack Start hosting guide:
cd web
pnpm add -D @netlify/vite-plugin-tanstack-startIn vite.config.ts, add the Netlify plugin alongside @tanstack/react-start/plugin/vite and @vitejs/plugin-react. Order can match TanStack Start — Netlify hosting.
Same stack as before (Tailwind v4, shadcn, optional GSAP, etc.). Install inside web/:
pnpm add @radix-ui/react-slot class-variance-authority clsx tailwind-merge lucide-react
pnpm add -D tailwindcss @tailwindcss/vite tw-animate-css shadcn
# optional: cssstudio for dev CSS studioConditional: gsap, @rive-app/canvas, framer-motion — only if needed.
Initialize Tailwind v4 and shadcn per their current docs (TanStack Start uses Vite under the hood). Use components.json from templates as a reference; merge paths with TanStack’s src/ layout.
Copy patterns from templates/site-fonts.css, templates/marketing.css, MarketingSiteRoot.tsx into web/src/. Import global styles from routes/__root.tsx (or the documented root layout file) so SSR receives the same CSS as the client.
The Webflow export’s index.html is no longer the HTML shell once you use TanStack Start — the document comes from web/src/routes/__root.tsx. You must manually lift:
<title>,<meta name="description">, Open Graph / Twitter tags,theme-color<link rel="icon">/ shortcut icon, apple-touch-icon (paths underpublic/stay/images/...)- Third-party analytics (
<script defer …>), unless the user asked to omit them
Centralize strings in something like web/src/site/seo.ts (templates/site-seo.example.ts) and return meta / links from the root route’s head() so SSR and social previews match the old site. <title> must be { title: siteSeo.title } inside the meta array — a top-level title on the head() return object is ignored by TanStack Router. See gotchas.md § The export’s <head> does not migrate by itself.
Also copy global * rules from css/webflow.css (or the <style> blocks in the export index.html) into your global stylesheet — at minimum -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale. Webflow relies on these for consistent text weight on macOS; TanStack/Tailwind shells omit them by default. See gotchas.md § Copy Webflow’s global font-smoothing.
Files in web/public/ are served at the site root (Vite behavior; same as the TanStack Start SEO guide). If the migration needs crawlers to discover URLs or a robots.txt policy, add web/public/sitemap.xml and/or web/public/robots.txt. They require no head() wiring.
Use templates/sitemap.xml.example and templates/robots.txt.example as starting points; replace hostnames and paths with the production domain and the routes you actually ship. For many pages or build-time discovery, prefer TanStack’s prerender + sitemap options in the Vite plugin; for CMS-backed URLs, use a server route (see the guide).
When porting the export into __root.tsx, do not blindly copy data-wf-page / data-wf-site on <html>. Confirm with a repo search that no data-wf- selectors exist in web/src/styles; then omit those attributes — they are editor metadata and trigger false “Webflow” hits in stack detectors.
Do keep the w-mod-js / w-mod-touch / w-mod-ix bootstrap (inline script or equivalent) if webflow.css or bundled CSS still targets html.w-mod-*. Do set generator meta to the real stack and move OG/Twitter images off uploads-ssl.webflow.com when you want social previews and URLs to match your domain.
See gotchas.md § Stack detection and rules/stack-detection-webflow.mdc.
cd web && npx shadcn@latest add button tooltipUse shadcn as a starting point only when no project SOT exists yet. Step 10g still requires hand-owned Button (or wrapper) with export-faithful classes and Storybook variant stories — do not leave raw w-button in routes.
Install Storybook inside web/ after global CSS is wired (step 10). Use the official generator for your framework (Vite + React; TanStack Start projects use the same Vite app under web/).
cd web
npx storybook@latest initPreview CSS parity (mission critical): web/.storybook/preview.ts must import the same chain as production — typically styles.css → marketing barrel → optional embed utilities. See templates/storybook-preview.example.ts.
Verify:
cd web && npm run storybookA primitive or section story must not look correct only because Storybook imports extra CSS production does not load.
Preview-only decorators: If a decorator adds a page shell (dark background, padding), use a class that never exists on production (e.g. .storybook-marketing-page-root). Do not inflate CSS custom properties on variant classes routes mount — see rules/react-sot-storybook-parity.mdc.
Before building page sections or routes:
- Grep export for repeating UI chrome (
.button,.w-button, heading classes, chips). - Create one component per pattern under
web/src/components/ui/. - Add variant stories under
Components/UI/<Name>. - Routes and future section SOTs import these — no export button markup.
Checklist: checklists/ui-primitives-pass.md. Rule: rules/react-sot-ui-primitives.mdc.
Same as Vite SPA:
cp -R <root>/images/* web/public/images/cp <root>/fonts/* web/public/fonts/(woff2/woff/ttf as exported)- Rewrite CSS URLs to
/images/...,/fonts/...
See gotchas.md § Fonts.
Same verbatim Webflow CSS strategy as before. Barrel marketing.css with @import "./site-fonts.css" first.
Load order vs Webflow: If index.html ships *.webflow.css in <head> and then <style> utilities in body (often .hide { display: none !important }), preserve that sequence in the barrel: put global-embed.css (or whatever holds those extracts) after the compiled *.webflow.css imports — not before. Reversed order makes hide nodes visible. See gotchas.md § Designer-hidden nodes: .hide and stylesheet order.
Import the marketing barrel in the root layout (__root.tsx) so SSR includes styles:
import '../styles/marketing.css'(Adjust path to match your file layout.)
Keep Tailwind entry (src/styles.css or index.css — follow TanStack starter) and align @theme font tokens with site-fonts.css.
For each repeating export section on the migration slice:
- Build section component + co-located CSS.
- Add Storybook story (
Components/Page sections/…). - Compose 10g primitives inside the section.
Checklist: checklists/section-sot-pass.md.
Do not paste export section HTML into routes when a section story exists.
- Home page:
src/routes/index.tsximports section SOT components in export order. __root.tsx: wrap withMarketingSiteRoot/ providers (e.g.TooltipProviderif using shadcn tooltips).
Match Webflow index.html section order. Keep wrapper classes (page-wrapper, main-wrapper, etc.) verbatim where still needed for CSS.
Same as legacy playbook: className on layout wrappers only where needed; SOT components receive props, not export class strings for chrome. Preserve export copy character-for-character unless the user asks to edit.
Before declaring a route done: checklists/sot-component-pass.md.
Same mapping table as before (nav, tooltips, iframes, touch class, analytics if any). Hooks stay in src/hooks/ and run in client components / useEffect.
TanStack Start renders on the server. GSAP, window, document, and browser-only APIs must not run during SSR.
- Put GSAP init only in
useEffector in files marked withclient-onlypatterns per TanStack Start docs (e.g.use clientboundaries if using that convention, or lazy client components). - Keep
gsap.context()+ctx.revert()as in rules/gsap-in-react.mdc.
(Same code as before — runs only after hydration.)
Same “don’t rewrite in Framer Motion” and Webflow Interactions 2.0 notes as the previous playbook.
Root netlify.toml — use templates/netlify.toml in this skill. Typical shape:
[build]
base = "web"
command = "npm ci && npm run build"
publish = "dist/client"With [build] base = "web", publish is relative to base (so on disk that is web/dist/client from the repo root). Do not set publish = "web/dist/client" in the same file — that would incorrectly resolve to web/web/dist/client.
Mission critical — Netlify dashboard: In Build & deploy → Build settings, leave Runtime, Base directory, Package directory, Build command, Publish directory, and Functions directory empty / Not set. Filled UI fields override netlify.toml and routinely cause 404s, missing SSR functions, or netlify/functions mismatches. See gotchas.md § Mission critical: Netlify UI.
Always run npm run build locally in web/ once and confirm the output folder (dist/client vs dist/...). TanStack Start + Netlify plugin may change paths between versions — adjust publish to match actual output.
Lockfile: npm ci in netlify.toml assumes web/package-lock.json is committed and matches web/package.json. After dependency edits: npm install in web/, then rm -rf node_modules && npm ci smoke-test before push; prefer pinning @tanstack/* over latest on repos you ship. See gotchas.md § npm ci, lockfiles, and "latest", shipping.md § Lockfile and CI parity.
Do not ship with only npm ci --prefix web && npm run build --prefix web from the repo root without [build] base in netlify.toml — the Netlify Vite plugin needs the build to run with cwd = web/ so SSR/functions under .netlify/ deploy correctly (see gotchas.md § Netlify).
Install @netlify/vite-plugin-tanstack-start in web/ (see §3.3).
Copy templates/web-netlify.toml to web/netlify.toml so vite dev does not resolve web/web when the Netlify Vite plugin uses web/ as config root. Keep repo-root netlify.toml for production (see §9).
After visual parity, optionally improve Lighthouse / PageSpeed:
- Font preloads for hero-critical woff2 files — templates/site-font-preload.example.ts; wire into
__root.tsxhead()before the main stylesheet. - Deferred lightweight analytics — if the site uses Plausible, see templates/PlausibleLoader.tsx (
VITE_PLAUSIBLE_DOMAINinweb/.env). For other scripts, use the same load + idle injection idea or the vendor’s documented snippet; skip if the user wants no analytics. - CLS overrides — last-imported CSS — templates/performance-overrides.example.css; tune selectors per that export’s markup.
- LCP images —
loading="eager"+fetchPriority="high"for obvious hero images; keeplazybelow the fold.
Details and limits (e.g. render-blocking CSS): gotchas.md § Core Web Vitals.
Mandatory on first migration when the user wants the project online:
- Follow shipping.md end-to-end.
- Prefer GitHub MCP
create_repository+git push. - If MCP auth fails, use
gh auth loginor manual repo creation — do not block silently. - Connect Netlify to the GitHub repo (dashboard or
netlify login+netlify init). Clear all Build settings overrides in the Netlify UI — see §9 / gotchas.md § Mission critical: Netlify UI. - Share the Netlify URL with the user.
mkdir -p .cursor/rules
cp ./rules/*.mdc .cursor/rules/ # from cloned skill repo; or ~/.cursor/skills/webflow-to-react/rules/ if installed globallyRun checklists/cleanup-before-done.md.
- No more Webflow re-exports as source of truth.
- Add new marketing routes with TanStack Router as the site grows (
/blog,/legal, …). - Progressive CSS → utility refactors one section at a time.
If the user explicitly requests a plain Vite + React SPA (no SSR):
npm create vite@latest web -- --template react-ts- Follow older steps: single
main.tsx,index.htmlatweb/,publish = "dist"in Netlify. - Skip TanStack Start–specific plugins and
dist/clientpaths.
Do not use this appendix unless the user opts out of TanStack Start.