Migrating a Framer portfolio to Next.js

Migrate a Framer portfolio to Next.js: projects CMS, case studies, contact form and animations, with a checklist in the order you'll work.

Published
Updated
Reading time
6 min
Deframer cover: “Your portfolio, case by case.” Beside it, four tiles, Projects, Case studies, Contact form and Animations, with Case studies selected
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

  1. 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.
  2. 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):

json
{
  "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:

  1. Add the record to src/cms/project.json with a unique slug.
  2. Add { slug: "your-new-slug" } to generateStaticParams in the detail route.
  3. Add the item's title, description and image to the route's metadata.json.
  4. Run npm run build locally, 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:

  1. Copy .env.example to .env.local.
  2. Set FORMS_CONTACT_WEBHOOK_URL (just this form) or FORMS_WEBHOOK_URL (every form) to a webhook from a tool such as Zapier, Make, n8n or Slack.
  3. 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:

sh
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:

ts
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

  1. npm install, npm run dev, click through every page.
  2. Check each case study's title and social image (the route's metadata.json).
  3. Wire up and test the contact form.
  4. Set metadataBase in the root layout to your production domain if it changes.
  5. Update the domain inside public/robots.txt and public/sitemap.xml if needed.
  6. Deploy to any host that runs Next.js, then point your domain at it.
  7. 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.

Keep reading

All posts
  • Deframer cover: “No export? Four ways out.” Beside it, four tiles, Rebuild, Snapshot, Converter and Reverse proxy, with Converter selected

    Export6 min read

    Can you export code from Framer?

    Framer has no code export. Here are the four real routes to your site's code (rebuild, HTML snapshot, converter, reverse proxy) and how to pick one.

  • Deframer cover: “To Next.js, step by step.” Beside it, a list of Framer, Next.js, TypeScript and Deploy anywhere with Next.js selected

    Migration6 min read

    How to export a Framer site to Next.js

    Export a Framer site to Next.js step by step: convert, compare, verify ownership and download an App Router TypeScript project, then check it.