On this page10 sections
To migrate a Framer portfolio to Next.js, you publish the site in Framer, convert it in Deframer, compare the result, verify ownership and download a Next.js App Router project in TypeScript. Your projects collection becomes a JSON file with a typed schema, each case study becomes a prerendered route, the contact form gets its own API route that you connect to a webhook, and animations keep running through the motion package. The rest of the work is a checklist: confirm the content, wire up the form, set the domain, and deploy.
This guide is written for a designer or developer moving their own portfolio. It follows the order you'd actually work in.
What a typical Framer portfolio contains
Most portfolios built in Framer share a shape:
- A home page with a hero and a grid of selected projects
- A projects CMS collection (title, slug, year, client, cover image, gallery images, a rich-text body)
- A case-study detail page that renders one project
- An about page, often with a photo and a list of clients or awards
- A contact form, sometimes also a newsletter field in the footer
- Hover effects, appear animations and page transitions that give the site its feel
Each of those has a clear home in the converted project, which makes portfolios a good fit for a conversion.
Before you convert
- Publish the latest version. Deframer reads the published site, the same files a browser loads, so anything only in the editor won't be in the project.
- Decide on assets. Portfolios are image-heavy. With "Include assets locally" off (the default), images, fonts and videos keep loading from Framer's CDN; the download is smaller and the conversion faster. With it on, every Framer-hosted asset is downloaded into
public/with readable names, and the site no longer depends on Framer at all. If a very large site doesn't fit, retry with the option off. CDN vs. local assets goes through the trade-offs.
Deframer is an independent tool and isn't affiliated with Framer.
Convert, then compare
Paste the site's published address (not the editor link) and convert. Progress moves through queued, fetching, modules, converting and packaging. When a live preview is available, the compare view puts the original and the converted site side by side at 1440, 768 and 390 pixels wide, with synced scrolling, for up to 10 minutes; otherwise, check the same things with npm run dev once you've downloaded the project. For a portfolio, spend your time on:
- The project grid at each width (card order, hover states, cover crops)
- One or two case studies, top to bottom
- Page transitions between home and a project
Also read the notices on the result page. "Components that render only in the browser" lists components that need the browser to load or render; they appear once the page has loaded rather than in the server HTML, so check them in the preview or locally. "Files that couldn't be downloaded" is worth checking if you turned local assets on.
Before you download
Verify ownership. Converting doesn't need it, but downloading does. Deframer gives you a one-line meta tag (<meta name="deframer-verification" content="dfv_…">). Paste it into Framer's Site Settings → Custom Code, publish, and click Check again. The first download uses one credit; if the code isn't found, none is used. The converter strips the tag from the project, and you can remove it from Framer afterwards.
The projects CMS
Your collection becomes two files, for example src/cms/project.json (the records) and src/cms/project.ts (a plain schema and a JsonCollection from the kit). Field names are readable, image fields carry src, srcSet, pixelWidth, pixelHeight and alt, and rich text becomes a JSON tree the kit renders, with links as paths and embedded components by name.
A record looks like this (an illustrative example):
{
"id": "cImcpXX5N",
"title": "Northwind Rebrand",
"slug": "northwind-rebrand",
"year": "2025",
"cover": { "src": "https://framerusercontent.com/images/….jpg", "srcSet": "… 512w, … 1024w", "pixelWidth": 1024, "pixelHeight": 768, "alt": "Northwind logo on a poster" }
}Your field names will match your own collection. To fix a typo in a case study, edit the JSON and rebuild. The CMS to JSON guide covers rich text and images in more depth.
Case-study pages
Each case study is prerendered from src/app/project/[slug]/page.tsx (the folder follows your site's URL). The route lists every item in generateStaticParams, sets dynamicParams = false so unknown slugs get the 404 page, and reads each item's title, description and social image from a metadata.json beside it.
Adding a new project
Because pages are prerendered, a new project is a code change plus a redeploy:
- Add the record to
src/cms/project.jsonwith a uniqueslug. - Add
{ slug: "your-new-slug" }togenerateStaticParamsin the detail route. - Add the item's title, description and image to the route's
metadata.json. - Run
npm run buildlocally, then deploy.
If you add projects often, a headless CMS may suit you better later; the project's record shape is a sensible starting schema.
The contact form
Framer's native forms no longer post to Framer's form service. Each form gets its own route, such as src/app/api/forms/contact/route.ts, with its fields in src/site/forms.ts and the shared handler in src/lib/forms.ts (a per-IP rate limit kept in memory, a honeypot spam filter, field validation, and delivery).
By default a submission is only logged by the server. To receive it:
- Copy
.env.exampleto.env.local. - Set
FORMS_CONTACT_WEBHOOK_URL(just this form) orFORMS_WEBHOOK_URL(every form) to a webhook from a tool such as Zapier, Make, n8n or Slack. - Set the same variables on your host.
To get an email instead, call your email provider's HTTP API from deliver in src/lib/forms.ts. Test locally:
curl -X POST http://localhost:3000/api/forms/contact \
-H "Content-Type: application/json" \
-d '{"fields":{"Name":"Test","Email":"test@example.com","Message":"Hello"}}'Use the field names from your own src/site/forms.ts. A form inside a third-party embed, such as a Typeform, keeps posting where it did.
Animations and the feel of the site
Transitions, appear effects, hover and press states, variants and page transitions carry over, played by motion/react and the kit. Springs keep their stiffness, damping and mass, so a card that lifted with a particular bounce still does:
const transition1 = { damping: 60, delay: 0, mass: 1, stiffness: 400, type: "spring" } as const;Page transitions and the site's reduced-motion setting are in src/site/routes.ts. See keeping animations with Motion for editing them.
Launch checklist
npm install,npm run dev, click through every page.- Check each case study's title and social image (the route's
metadata.json). - Wire up and test the contact form.
- Set
metadataBasein the root layout to your production domain if it changes. - Update the domain inside
public/robots.txtandpublic/sitemap.xmlif needed. - Deploy to any host that runs Next.js, then point your domain at it.
- Put the project in a Git repository, so future edits are tracked.
A portfolio usually needs one conversion, which is free, and one credit to download it. See pricing for the packs.
FAQ
Will my project grid and case studies look the same?
They're rebuilt from the published site's components and styles, at each breakpoint. Check them at 1440, 768 and 390 pixels: in the compare view when a live preview is available, or by running the project.
How do I add a new project after migrating?
Add the record to the collection's JSON file, add its slug to generateStaticParams and its entry to the route's metadata.json, then rebuild and deploy. Content changes are code changes in a converted project.
Where do contact form submissions go?
They're logged by the server and, once you set FORMS_WEBHOOK_URL or the form's own variable, POSTed as JSON to that webhook. For email, call your provider's API from deliver.
Do I own the converted code?
Yes. You own the converted code of your site, and Deframer claims no rights in it. Open-source packages remain under their own licenses.
